From 891fd0233ef0ac3165c0d91e698d08ffe947ebca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 05:30:37 +0000 Subject: [PATCH 01/61] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..abf4b05 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>android/.github:renovate-config" + ] +} From b458f3d15039d45ded04bb92f1e9fbb8b7d43a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 16 Apr 2026 12:51:28 +0200 Subject: [PATCH 02/61] Adds issue templates (#14) --- .github/ISSUE_TEMPLATE/bug_report.md | 25 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..af0172a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug Report +about: Report an issue with an existing skill. +title: '[Bug] ' +labels: bug +assignees: '' +--- + +### Affected Skill + + +### Description + + +### Expected Behavior + + +### Actual Behavior + + +### Steps to Reproduce + + +### Suggested Fix + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e30f9e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest a new skill to fill a gap in the LLM's knowledge. +title: '[Feature] ' +labels: enhancement +assignees: '' +--- + +> [!NOTE] +> Feature requests are accepted only if the proposed skill fills a verified gap in state-of-the-art LLMs. To assess this, please test your use case against both a fast model (e.g., Gemini 3 Flash) and a reasoning/thinking model (e.g., Gemini 3.1 Pro). + +### What is the gap in the LLM's knowledge? + + +### Proposed Skill + + +### Additional Context + From ab0fb3361176bb258fe464e2b8fb00766cc2645a Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:16:16 +0100 Subject: [PATCH 03/61] Update README (#16) --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index effd96e..e4cafad 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,31 @@ Android skills follow the [open-standard agent skills](https://agentskills.io/ho files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows. -To use Android skills, check -your agent's documentation. +To learn more, read the official documentation: + +- [Android skills](https://developer.android.com/tools/agents/android-skills) +- [Android CLI](https://developer.android.com/tools/agents/android-cli) +- [Android Studio](https://developer.android.com/studio/gemini/skills) + +### Install Android skills + +Use Android CLI to install Android skills to directories for all detected agents: + +``` +android skills add [--all] [--agent=] [--skill=] +``` + +If you don't have any existing agent directories and don't specify particular agents, the skills +will be installed for Gemini and Antigravity at `~/.gemini/antigravity/skills`. + +**Options:** + +- `--all` - Add all Android skills. If omitted (and `--skill` isn't specified), only the + `android-cli` skill will be installed. +- `--agent` - A comma-separated list of agents to install the skill for. If omitted, the skill will + be installed for all detected agents. +- `--skill` - Specific skill that you want to install. If omitted (and `--all` isn't specified), + only the `android-cli` skill will be installed. ## Disclaimer From 03146264e3cfa0ec28367a11daeb7ddb5c8f9e5a Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:21:55 +0100 Subject: [PATCH 04/61] Fix README (#17) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4cafad..cbc22b6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Android skills are a dedicated repository of AI-optimised, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and -guidance on Android development from [developer.android.com](developer.android.com). +guidance on Android development from [developer.android.com](https://developer.android.com). Android skills follow the [open-standard agent skills](https://agentskills.io/home) - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information From b281881e3fbe044d29b4ea757de06758a4ca81ec Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:28:30 +0100 Subject: [PATCH 05/61] Fix xml-layout-migration.md (#19) --- .../references/xml-layout-migration.md | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) 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 8e93794..425cbc2 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 @@ -24,7 +24,7 @@ Evaluate if the XML layout serves as a foundation-level design system component \* **Feature parity \& restriction:** Ensure the new composable enforces the same UI constraints as the original XML component, preventing unauthorized style overrides while maintaining the intended flexibility. Example before migration: -`xml +```xml ` + +``` Example after migration: -## ```kotlin +```kotlin @Composable fun RoundedBorderlessButton( -text: String, -onClick: () -> Unit, -modifier: Modifier = Modifier, -enabled: Boolean = true + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true ) { -TextButton( -onClick, modifier -.defaultMinSize(minWidth = dimensionResource(R.dimen.min_width)) -.padding( -start = dimensionResource(R.dimen.padding_2), -end = dimensionResource(R.dimen.padding_2) -), enabled, shape = RoundedCornerShape(8.dp), -colors = ButtonDefaults.textButtonColors( -contentColor = MaterialTheme.colorScheme.primary -) -) { -Text( -text = text, -style = MaterialTheme.typography.bodyMedium.copy( -fontFamily = FontFamily.SansSerif, -fontWeight = FontWeight.Medium -) -) -} + TextButton( + onClick, modifier + .defaultMinSize(minWidth = dimensionResource(R.dimen.min_width)) + .padding( + start = dimensionResource(R.dimen.padding_2), + end = dimensionResource(R.dimen.padding_2) + ), enabled, shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium + ) + ) + } } ``` From 04d6e21f536b3fc46a92495ea841cc9f3425c389 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Mon, 27 Apr 2026 08:51:56 +0000 Subject: [PATCH 06/61] Updates skills (2026-04-27 08:51) --- build/agp/agp-9-upgrade/SKILL.md | 14 +- .../build/releases/agp-9-0-0-release-notes.md | 15 +- build/agp/agp-9-upgrade/references/recipes.md | 3 +- .../interoperability-apis/compose-in-views.md | 8 +- .../interoperability-apis/views-in-compose.md | 13 +- ...setup-compose-dependencies-and-compiler.md | 43 +- .../references/xml-layout-migration.md | 57 +- .../navigation-3/migration-guide.md | 3 + .../navigation-3/recipes/conditional.md | 4 +- .../recipes/deeplinks-advanced.md | 2 + .../navigation-3/recipes/deeplinks-basic.md | 24 +- .../recipes/material-listdetail.md | 4 +- .../recipes/material-supportingpane.md | 4 +- .../navigation-3/recipes/modular-hilt.md | 2 +- .../recipes/multiple-backstacks.md | 6 +- .../navigation-3/recipes/passingarguments.md | 5 - .../navigation-3/recipes/results-event.md | 7 +- .../navigation-3/recipes/results-state.md | 6 +- .../navigation-3/recipes/scenes-listdetail.md | 4 +- .../navigation-3/recipes/scenes-twopane.md | 5 +- performance/r8-analyzer/SKILL.md | 3 +- .../enable-app-optimization.md | 13 +- .../testing/other-components/ui-automator.md | 312 +++++++ .../SKILL.md | 322 ++++++++ .../ai-glasses/guides/interaction/inputs.md | 67 ++ .../access-hardware-projected-context.md | 221 +++++ .../ai-glasses/check-capabilities.md | 47 ++ .../ai-glasses/first-activity.md | 278 +++++++ .../ai-glasses/notifications/behavior.md | 130 +++ .../notifications/start-activity.md | 77 ++ .../jetpack-compose-glimmer/buttons.md | 65 ++ .../jetpack-compose-glimmer/cards.md | 81 ++ .../jetpack-compose-glimmer/focus.md | 98 +++ .../jetpack-compose-glimmer/icons.md | 40 + .../jetpack-compose-glimmer/text.md | 44 + .../jetpack-compose-glimmer/title-chips.md | 52 ++ .../request-hardware-permissions.md | 177 ++++ .../references/button-samples-source.md | 176 ++++ .../references/button-source.md | 205 +++++ .../references/card-samples-source.md | 295 +++++++ .../references/card-source.md | 588 ++++++++++++++ .../references/deptheffect-source.md | 224 ++++++ .../references/deptheffectlevels-source.md | 105 +++ .../glimmersansflextypography-source.md | 270 +++++++ .../references/glimmertheme-source.md | 182 +++++ .../references/icon-source.md | 389 +++++++++ .../references/iconbutton-source.md | 127 +++ .../references/iconsizes-source.md | 78 ++ .../references/list-source.md | 394 +++++++++ .../references/listitem-source.md | 282 +++++++ .../references/liststate-source.md | 420 ++++++++++ .../references/material-hct-source.md | 217 +++++ .../references/projectedcontext-source.md | 265 ++++++ .../references/stack-source.md | 368 +++++++++ .../references/stackitemscope-source.md | 440 ++++++++++ .../references/stackstate-source.md | 325 ++++++++ .../references/surface-source.md | 761 ++++++++++++++++++ .../references/titlechip-samples-source.md | 101 +++ .../references/titlechip-source.md | 136 ++++ 59 files changed, 8501 insertions(+), 103 deletions(-) create mode 100644 performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md create mode 100644 xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build/agp/agp-9-upgrade/SKILL.md index 6436b33..c7b4f76 100644 --- a/build/agp/agp-9-upgrade/SKILL.md +++ b/build/agp/agp-9-upgrade/SKILL.md @@ -1,7 +1,8 @@ --- name: agp-9-upgrade description: Upgrades, or migrates, an Android project to use Android Gradle Plugin - (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) projects. + (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) + projects. license: Complete terms in LICENSE.txt metadata: author: Google LLC @@ -27,8 +28,7 @@ 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. +[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 @@ -48,11 +48,11 @@ version 2.3.6 or higher. If Hilt is used in the project, ensure it is on version 2.59.2 or higher. -### Step 2: Migrate to built-in Kotlin. +### Step 2: Migrate to built-in Kotlin See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information. -### Step 3. Migrate to the new AGP DSL. +### Step 3. Migrate to the new AGP DSL See [the guide](references/android/build/releases/agp-9-0-0-release-notes.md) for detailed information. @@ -62,8 +62,8 @@ 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, [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) -for detailed migration steps. +used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration +steps. ### Step 5. BuildConfig diff --git a/build/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md b/build/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md index b6a6e56..e6d3dbe 100644 --- a/build/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md +++ b/build/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md @@ -3,12 +3,17 @@ 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). +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. -The AGP upgrade assistant helps preserve existing behaviors when upgrading your -project whenever appropriate, allowing you to 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). ## Compatibility diff --git a/build/agp/agp-9-upgrade/references/recipes.md b/build/agp/agp-9-upgrade/references/recipes.md index c26d85f..5cf4a2d 100644 --- a/build/agp/agp-9-upgrade/references/recipes.md +++ b/build/agp/agp-9-upgrade/references/recipes.md @@ -4,8 +4,7 @@ migrated. ## Guidelines -- **DO NOT** search the web for examples of how to do this. Use the - **gradle-recipes** repository examples **only**. +- **DO NOT** search the web for examples of how to do this. Use the **gradle-recipes** repository examples **only**. - **DO NOT** use AGP internals in migrated code. - **DO** use only public APIs in migrated code. 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 0a0f7eb..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 @@ -1,3 +1,6 @@ +> [!NOTE] +> **Note:** Interoperability is supported for hybrid apps that use Compose and Views. For apps that are only in Compose, use the recommended Compose-only architecture with a single Activity and latest navigation libraries, like Navigation 3. + 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 @@ -61,7 +64,10 @@ The table below summarizes the different scenarios you can use | [`DisposeOnLifecycleDestroyed`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy.DisposeOnLifecycleDestroyed) | The Composition will be disposed when the provided [`Lifecycle`](https://developer.android.com/reference/androidx/lifecycle/Lifecycle) is destroyed. Interop scenario \* `ComposeView` in a Fragment's View. | | [`DisposeOnViewTreeLifecycleDestroyed`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | The Composition will be disposed when the `Lifecycle` owned by the `LifecycleOwner` returned by `ViewTreeLifecycleOwner.get` of the next window the View is attached to is destroyed. Interop scenario: \* `ComposeView` in a Fragment's View. \* `ComposeView` in a View wherein the Lifecycle is not known yet. | -## `ComposeView` in Fragments +## `ComposeView` in Fragments (Transitionary Step) + +> [!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) 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 9b6add2..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 @@ -4,6 +4,11 @@ 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` @@ -63,6 +68,9 @@ To embed an XML layout, use the 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 @@ -113,12 +121,15 @@ fun AndroidViewInLazyList() {
-## Fragments in Compose +## 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. +> [!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 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 22c39c5..b661b5b 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,20 +1,18 @@ ## Set up the Compose Compiler Gradle plugin -For Gradle users, you can use the Compose Compiler Gradle plugin to make setting -up and configuring Compose easier. +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). For an example migration, see the [Compose Samples -> PR](https://github.com/android/compose-samples/pull/1354) in the Compose samples. +> repository"](https://android-developers.googleblog.com/2024/04/jetpack-compose-compiler-moving-to-kotlin-repository.html). ### Set up with Gradle version catalogs -The following instructions outline how you can set up the Compose Compiler -Gradle plugin: +Set up the Compose Compiler Gradle plugin: -1. In your `libs.versions.toml` file, remove any reference to the Compose Compiler. -2. In the `versions` and `plugins` section, add the following new dependency: +1. In the `libs.versions.toml` file, remove any reference to the Compose Compiler. +2. In the `versions` and `plugins` sections, add the new dependency: [versions] kotlin = "2.3.10" @@ -25,36 +23,34 @@ Gradle plugin: // Add this line compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -1. In your project's root `build.gradle.kts` file, add the following snippet to the `plugins` section. +1. In the project's root `build.gradle.kts` file, add the following to the `plugins` section. plugins { // Existing plugins alias(libs.plugins.compose.compiler) apply false } -1. In each module that uses Compose, apply the plugin as follows. +1. In each module that uses Compose, apply the plugin: plugins { // Existing plugins alias(libs.plugins.compose.compiler) } -Your app should now build and compile if you are using the default set up. If -you had configured custom options on the Compose compiler, see the following +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 -following plugin to `build.gradle.kts` files associated with modules you use -Compose: +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.10" // this version matches your Kotlin version } -You might also need to add this classpath to your top-level project -`build.gradle.kts` file: +Add the classpath to your top-level project `build.gradle.kts` file: buildscript { dependencies { @@ -66,7 +62,7 @@ You might also need to add this classpath to your top-level project 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. +level: android { ... } @@ -79,6 +75,11 @@ 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.03.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. + Add the following definition to your app's `build.gradle` file: ### Groovy @@ -97,11 +98,7 @@ Add the following definition to your app's `build.gradle` file: } } -Setting 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) -block enables [Compose functionality](https://developer.android.com/develop/ui/compose/tooling) in Android Studio. - -Finally, add the Compose BOM and the subset of Compose library dependencies -you need to your dependencies from the following block: +Add the Compose BOM and the subset of Compose library dependencies: ### Groovy 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 425cbc2..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,43 +1,50 @@ ## 1. Structural analysis \& mapping -**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`). -\* The appropriate state management strategy for interactive elements. +**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`). +- The appropriate state management strategy for interactive elements. ## 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 **Do not use hard-coded values.** Follow these rules for styling: -\* **Token Alignment:** Cross-reference XML dimension, color, and style attributes with the existing Compose `Theme` (e.g., `MaterialTheme.colorScheme` or custom design system tokens). -\* **Reuse over Creation:** If matching values exist in the current Compose theme, reuse them. If a value is missing but required for the design, define it within the theme structure rather than hard-coding it in the Composable. -\* **Project Consistency:** You **MUST** strictly adhere to existing code conventions, naming standards, and implementation patterns found in the project. **Prioritize** project-specific reusable components over generic Material defaults. + +- **Token Alignment:** Cross-reference XML dimensions, colors, and style attributes with the existing Compose `Theme` (e.g., `MaterialTheme.colorScheme` or custom design system tokens). +- **Reuse over Creation:** If matching values exist in the current Compose theme, reuse them. If a value is missing but required for the design, define it within the theme structure rather than hard-coding it in the Composable. +- **Project Consistency:** You **MUST** strictly adhere to existing code conventions, naming standards, and implementation patterns found in the project. **Prioritize** project-specific reusable components over generic Material defaults. ## 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: -\* **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. -\* **Feature parity \& restriction:** Ensure the new composable enforces the same UI constraints as the original XML component, preventing unauthorized style overrides while maintaining the intended flexibility. +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. +- **Feature parity \& restriction:** Ensure the new composable enforces the same UI constraints as the original XML component, preventing unauthorized style overrides while maintaining the intended flexibility. Example before migration: -```xml - -``` + + Example after migration: + ```kotlin @Composable fun RoundedBorderlessButton( @@ -68,6 +75,10 @@ fun RoundedBorderlessButton( } ``` +
+ +*** ** * ** *** + ## 5. Output requirements - Provide the full Kotlin file content. 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 88223f8..ba86f3c 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 @@ -8,6 +8,9 @@ To migrate your app from [Navigation 2](https://developer.android.com/guide/navi 6. Replace `NavHost` with `NavDisplay`. 7. Remove Navigation 2 dependencies. +> [!IMPORTANT] +> **Important:** We released an agent skill to help you install and migrate to Jetpack Navigation 3. Try out the skill from the [Android skills repository](https://github.com/android/skills). +
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 7ff23db..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 @@ -48,11 +48,11 @@ import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSerializable +import androidx.compose.runtime.setValue import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey @@ -63,6 +63,7 @@ import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentYellow +import com.example.nav3recipes.ui.setEdgeToEdgeConfig import kotlinx.serialization.Serializable @@ -103,6 +104,7 @@ private data class Login( class ConditionalActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md index 62a9439..f241291 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md @@ -46,6 +46,7 @@ import com.example.nav3recipes.common.deeplink.LIST_LOCATIONS import com.example.nav3recipes.common.deeplink.MenuDropDown import com.example.nav3recipes.common.deeplink.PaddedButton import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.ui.setEdgeToEdgeConfig internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com" @@ -68,6 +69,7 @@ internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com" class AdvancedCreateDeepLinkActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md index f96f642..66312de 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md @@ -81,18 +81,19 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher -import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern -import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult -import com.example.nav3recipes.deeplink.basic.util.KeyDecoder +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FriendsList +import com.example.nav3recipes.common.deeplink.LIST_USERS import com.example.nav3recipes.common.deeplink.TextContent import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FriendsList -import com.example.nav3recipes.common.deeplink.LIST_USERS +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher +import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern +import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest +import com.example.nav3recipes.deeplink.basic.util.KeyDecoder +import com.example.nav3recipes.ui.setEdgeToEdgeConfig /** * Parses a target deeplink into a NavKey. There are several crucial steps involved: @@ -131,6 +132,7 @@ class MainActivity : ComponentActivity() { ) override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) // retrieve the target Uri @@ -211,7 +213,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.PaddedButton import com.example.nav3recipes.common.deeplink.EMPTY import com.example.nav3recipes.common.deeplink.EntryScreen import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN @@ -224,11 +225,13 @@ import com.example.nav3recipes.common.deeplink.LOCATION_CA import com.example.nav3recipes.common.deeplink.LOCATION_US import com.example.nav3recipes.common.deeplink.MenuDropDown import com.example.nav3recipes.common.deeplink.MenuTextInput +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.ui.setEdgeToEdgeConfig /** * This activity allows the user to create a deep link and make a request with it. @@ -254,6 +257,7 @@ import com.example.nav3recipes.common.deeplink.TextContent */ class CreateDeepLinkActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { 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 60a7fc6..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 @@ -47,7 +47,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy @@ -88,7 +88,7 @@ class MaterialListDetailActivity : ComponentActivity() { // Override the defaults so that there isn't a horizontal space between the panes. // See b/418201867 - val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() val directive = remember(windowAdaptiveInfo) { calculatePaneScaffoldDirective(windowAdaptiveInfo) .copy(horizontalPartitionSpacerSize = 0.dp) 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 97727bf..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 @@ -49,7 +49,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior import androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy @@ -90,7 +90,7 @@ class MaterialSupportingPaneActivity : ComponentActivity() { // Override the defaults so that there isn't a horizontal or vertical space between the panes. // See b/444438086 - val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() val directive = remember(windowAdaptiveInfo) { calculatePaneScaffoldDirective(windowAdaptiveInfo) .copy(horizontalPartitionSpacerSize = 0.dp, verticalPartitionSpacerSize = 0.dp) 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 e22b0c1..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 @@ -86,8 +86,8 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier -import androidx.navigation3.ui.NavDisplay import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.ui.setEdgeToEdgeConfig import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject 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 b7f7ebb..81cd794 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 @@ -378,7 +378,7 @@ fun EntryProviderScope.featureASection( mutableIntStateOf(0) } - Button(onClick = dropUnlessResumed { count++ }) { + Button(onClick = { count++ }) { Text("Value: $count") } } @@ -402,7 +402,7 @@ fun EntryProviderScope.featureBSection( var count by rememberSaveable { mutableIntStateOf(0) } - Button(onClick = dropUnlessResumed { count++ }) { + Button(onClick = { count++ }) { Text("Value: $count") } } @@ -427,7 +427,7 @@ fun EntryProviderScope.featureCSection( mutableIntStateOf(0) } - Button(onClick = dropUnlessResumed { count++ }) { + Button(onClick = { count++ }) { Text("Value: $count") } } 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 292bee3..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 @@ -293,17 +293,12 @@ import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import org.koin.android.ext.koin.androidContext import org.koin.compose.KoinApplication import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.context.GlobalContext -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin import org.koin.core.module.dsl.viewModelOf import org.koin.core.parameter.parametersOf import org.koin.dsl.koinConfiguration import org.koin.dsl.module -import org.koin.mp.KoinPlatform data object RouteA data class RouteB(val id: String) 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 068d0e5..c105669 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 @@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable data object Home : NavKey @Serializable -class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt +class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt ``` ``` @@ -94,7 +94,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/Person.kt +data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/Person.kt ``` ``` @@ -252,7 +252,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel @@ -265,10 +264,12 @@ import com.example.nav3recipes.results.common.HomeViewModel import com.example.nav3recipes.results.common.Person import com.example.nav3recipes.results.common.PersonDetailsForm import com.example.nav3recipes.results.common.PersonDetailsScreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig class ResultEventActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { 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 1f9323e..d2d349a 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 @@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable data object Home : NavKey @Serializable -class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt +class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt ``` ``` @@ -94,7 +94,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/d2a2288a393dfa373e02b04c48c483cd9add9dbf/app/src/main/java/com/example/nav3recipes/results/common/Person.kt +data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/Person.kt ``` ``` @@ -215,10 +215,12 @@ import com.example.nav3recipes.results.common.HomeScreen import com.example.nav3recipes.results.common.Person import com.example.nav3recipes.results.common.PersonDetailsForm import com.example.nav3recipes.results.common.PersonDetailsScreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig class ResultStateActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { 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 5ed49a8..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 @@ -39,7 +39,7 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf @@ -123,7 +123,7 @@ val LocalBackButtonVisibility = compositionLocalOf { true } @Composable fun rememberListDetailSceneStrategy(): ListDetailSceneStrategy { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) 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 a613c9d..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 @@ -16,7 +16,7 @@ package com.example.nav3recipes.scenes.twopane import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -67,7 +67,7 @@ data class TwoPaneScene( @Composable fun rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val windowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass return remember(windowSizeClass) { TwoPaneSceneStrategy(windowSizeClass) @@ -148,7 +148,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.material3.Button diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 7573aeb..c2265ba 100644 --- a/performance/r8-analyzer/SKILL.md +++ b/performance/r8-analyzer/SKILL.md @@ -27,8 +27,7 @@ metadata: - If refining the rule, give instructions on finding a narrower and specific keep rule using the [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md). - If removing, provide reasoning on why it needs to be removed. - \[ \] Step 8: After keep analysis, order the keep rule analysis based on the impact to the codebase hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) -- \[ \] Step 9: Advise the user to run tests using [UI - automator](https://developer.android.com/training/testing/other-components/ui-automator) to assess that there is no issue with the suggested changes, concentrating on the packages where keep rules will be affected. +- \[ \] Step 9: Advise the user to run tests using [UI automator](references/android/training/testing/other-components/ui-automator.md) to assess that there is no issue with the suggested changes, concentrating on the packages where keep rules will be affected. ## Mandatory rules 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 d9f6ac1..d78158e 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 @@ -12,6 +12,9 @@ performance, and more. To your users, this means: > **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] +> **Important:** We released an agent skill that you can use to improve your app performance with R8. Try out the skill from the [Android skills repository](https://github.com/android/skills). + ## R8 optimization overview R8 uses a multi-phase process to optimize your app for size and speed. Key @@ -27,8 +30,8 @@ operations include the following: 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. + 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 @@ -119,8 +122,8 @@ removed. ### Enable optimized resource shrinking -To turn on the new optimized resource shrinking pipeline for a version of AGP -before 9.0.0, 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: android.r8.optimizedResourceShrinking=true @@ -167,7 +170,7 @@ the Android Gradle Plugin (AGP) and the R8 compiler. |---|---| | 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 | **Resource shrinking:** Initial support added (Off by default. Enable using `isShrinkResources`). Resource shrinking works in tandem with R8 to identify and remove unused resources effectively. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). | +| 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 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 new file mode 100644 index 0000000..2b5b253 --- /dev/null +++ b/performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md @@ -0,0 +1,312 @@ +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 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)` +- 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): + +### Kotlin + + dependencies { + ... + androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05") + } + +### Groovy + + dependencies { + ... + androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05" + } + +## Core API concepts + +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. + + uiAutomator { + // All your UI Automator actions go here + startApp("com.example.targetapp") + onElement { textAsString() == "Hello, World!" }.click() + } + +### 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. + +- `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() + + // Find a UI element by its resource ID + onElement { viewIdResourceName == "my_button_id" }.click() + + // Allow a permission request + watchFor(PermissionDialog) { + 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. + + 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. + + // Get all items in a list Ui element + val listItems = onElements { className == "android.widget.TextView" && isClickable } + listItems.forEach { it.click() } + +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. + + // Find a parent Ui element with ID "first", then its child with ID "second", + // then its grandchild with ID "third", and click it. + onElement { viewIdResourceName == "first" } + .onElement { viewIdResourceName == "second" } + .onElement { viewIdResourceName == "third" } + .click() + +- 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() + + // Find a Ui element with a custom timeout of 10 seconds + onElement(10_000) { textAsString() == "Long loading text" }.click() + +### Interact with UI elements + +Interact with UI elements by simulating clicks or setting text in editable +fields. + + // Click a Ui element + onElement { textAsString() == "Tap Me" }.click() + + // Set text in an editable field + onElement { className == "android.widget.EditText" }.setText("My input text") + + // Perform a long click + onElement { contentDescription == "Context Menu" }.longClick() + +## Handle app states and watchers + +Manage the lifecycle of your app and handle unexpected UI elements that might +appear during your tests. + +### App lifecycle management + +The APIs provide ways to control the state of the app under test: + + // Start a specific app by package name. Used for benchmarking and other + // self-instrumenting tests. + startApp("com.example.targetapp") + + // Start a specific activity within the target app + startActivity(SomeActivity::class.java) + + // Start an intent + startIntent(myIntent) + + // Clear the app's data (resets it to a fresh state) + clearAppData("com.example.targetapp") + +### 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. + + import androidx.test.uiautomator.PermissionDialog + + @Test + fun myTestWithPermissionHandling() = uiAutomator { + startActivity(MainActivity::class.java) + + // Register a watcher to click "Allow" if a permission dialog appears + watchFor(PermissionDialog) { clickAllow() } + + // Your test steps that might trigger a permission dialog + onElement { textAsString() == "Request Permissions" }.click() + + // Example: You can register a different watcher later if needed + clearAppData("com.example.targetapp") + + // Now deny permissions + startApp("com.example.targetapp") + watchFor(PermissionDialog) { clickDeny() } + 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. + +### 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. + +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. + + // Wait for the entire active window to become stable + activeWindow().waitForStable() + + // Wait for a specific Ui element to become stable (e.g., after a loading animation) + onElement { viewIdResourceName == "my_loading_indicator" }.waitForStable() + +> [!NOTE] +> **Note:** In most cases, `waitForStable()` isn't strictly necessary when using `onElement { ... }` because `onElement` already includes a timeout. Use `waitForStable()` primarily in combination with `onElements { ... }` to verify that all UI elements are visible, when you know that the UI is in an unstable state, or for specific screenshot testing scenarios where you need the UI to completely settle before capturing. `waitForStable()` works by waiting until no changes are detected in the accessibility tree for a set period. Note that this UI stability check doesn't guarantee that the app is fully idle, as background tasks might still be running. + +## 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. + +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. + +## Advanced features + +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. + + // Find the first window that is in Picture-in-Picture mode + val pipWindow = windows() + .first { it.isInPictureInPictureMode == true } + + // Now you can interact with elements within that specific window + pipWindow.onElement { textAsString() == "Play" }.click() + +### 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. + + uiautomator { + // Take a screenshot of the entire active window + val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot() + fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png")) + + // Take a screenshot of a specific UI element (e.g., a button) + val buttonBitmap: Bitmap = onElement { viewIdResourceName == "my_button" }.takeScreenshot() + buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png")) + + // Example: Take a screenshot of a PiP window + val pipWindowScreenshot = windows() + .first { it.isInPictureInPictureMode == true } + .takeScreenshot() + pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png")) + } + +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. + + uiAutomator { + startApp("com.example.targetapp") + + val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results + val file = reporter.addNewFile( + filename = "my_screenshot", + title = "Accessible button image" // Title that appears in Android Studio test results + ) + + // Take a screenshot of an element and save it using the reporter + onElement { textAsString() == "Accessible button" } + .takeScreenshot() + .saveToFile(file) + + // Report the artifacts to instrumentation, making them visible in Android Studio + reporter.reportToInstrumentation() + } + +## 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: + +| Action type | Old UI Automator method | New UI Automator method | +|---|---|---| +| Entry point | `UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())` | Wrap test logic in the `uiAutomator { ... }` scope. | +| Find UI elements | `device.findObject(By.res("com.example.app:id/my_button"))` | `onElement { viewIdResourceName == "my\_button" }` | +| Find UI elements | `device.findObject(By.text("Click Me"))` | `onElement { textAsString() == "Click Me" }` | +| Wait for idle UI | `device.waitForIdle()` | Prefer `onElement`'s built-in timeout mechanism; otherwise, `activeWindow().waitForStable()` | +| Find child elements | Manually nested `findObject` calls | `onElement().onElement()` chaining | +| Handle permission dialogs | `UiAutomator.registerWatcher()` | `watchFor(PermissionDialog)` | \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md new file mode 100644 index 0000000..85c8dea --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -0,0 +1,322 @@ +--- +name: display-ai-glasses-with-jetpack-compose-glimmer +description: Provides guidelines for developing projected applications for Android + Display AI Glasses using the Jetpack Compose Glimmer UI toolkit. This skill covers + foundational Glimmer design principles, workflows for implementing Jetpack Compose + Glimmer, and interaction models for the glasses form factor. Use this skill to build + an Android XR Augmented Experience application with Jetpack Compose Glimmer that + adheres to the Glimmer design system for optimized Glasses styling. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - Jetpack Compose Glimmer + - AI Glasses + - Display AI Glasses + - Projected Activity + - GlimmerTheme + - Additive Display + - Android XR - Augmented Experiences +--- + +## Glossary + +| Term | Definition | +|---|---| +| **AI Glasses** | All-day wear, hands-free devices that provide access to information. Some have a display and some are audio-only. Equipped with speakers, a camera, and a microphone. | +| **Display AI Glasses** | AI Glasses with the addition of a small, private display for glanceable visuals that harmonize with audio output. | +| **Jetpack Compose Glimmer** | A Compose UI toolkit for building augmented Android XR experiences, optimized for Display AI Glasses. It provides components, theming, and behaviors for transparent displays. | +| **Projected Activity** | An Android `Activity` that runs on a host device (phone) but its UI and interactions are projected to a connected AI glasses device. | +| **Projected Device** | An XR device connected to an Android-powered device (host). Host projects the application content to the Projected device and let users interact with it. | +| **GlimmerTheme** | The root provider for styling tokens, including GlimmerColors, GlimmerTypography, and GlimmerShapes. | +| **Additive Display** | A display technology where black (#000000) is rendered as 100% transparent. UI is built by adding light to the environment. Display AI Glasses have an additive display. | +| **Augmented Experiences** | Android XR experiences that enhance a user's focus and presence in the real world. They are lightweight and additive, helping users while they are on-the-go | +| **Visual Angle** | A unit of measurement for perceived size in XR. The minimum readable text size is 0.6 degrees (approx. 18sp at 1 m). | + +## Prerequisites + +- Mobile project should target minSDK 37 or higher. If the target SDK is lower than 37, increase the SDK to 37. AI Glasses should target API 36 or higher. + +## Core Constraints + +- **Don't:** Use `MaterialTheme` or Material Components. +- **Do:** Use `GlimmerTheme` and Jetpack Compose Glimmer Components. + +## Limitations + +- Specific hardware device models or their unique capabilities are not detailed. +- **Elevation:** Standard Material 3 shadow or elevation modifiers are not supported. You must use `ShadowScope` or `Depth` tokens from Jetpack Compose Glimmer. +- **Minimum Size:** The absolute minimum size for readable text is 18sp (0.6°). Anything smaller will fail legibility checks. +- **Text Weight:** Avoid "Thin" or "Hairline" weights; they cause shimmering and aliasing on additive AR lenses. + +## 1. Set up dependencies + +- **Setup Projected Activity:** First you need to create a new projected activity for your AI glasses app. If the project doesn't already have one, see [Create your first activity for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. +- **Mobile App Integration:** If the project contains an existing mobile app, you must create a new Glasses Activity dedicated to rendering Glimmer UI. For detailed configuration, heavily reference [Create your first activity + for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the AI Glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. +- **UI Library:** Identify if the project has the `androidx.xr.glimmer:glimmer` library, if not it must be added to the project. See [Declaring Jetpack Compose Glimmer Dependencies](https://developer.android.com/jetpack/androidx/releases/xr-glimmer#declaring_dependencies) to fetch the latest dependency version. +- **Theming:** All Glimmer components must be wrapped within the `GlimmerTheme` composable to ensure correct token resolution. +- **Mandatory black background:** AI Glasses use additive displays. Any non-black color in the background blocks the real world. **You must always** set a pure black background (`Modifier.background(Color.Black)`) on the root container of your Projected Activity. +- **Font:** The default font is Google Sans Flex. Use `androidx.xr.glimmer.googlefonts` library with the default type styles unless otherwise specified. Use `createGoogleSansFlexTypography` to create a `Typography` instance with the Google Sans Flex configuration. Provide this `Typography` instance as normal through `GlimmerTheme`. Use [references/glimmersansflextypography-source.md](references/glimmersansflextypography-source.md) for configuration. +- **Hardware Capabilities:** Different types of AI glasses have different capabilities. To check for these at runtime, see the [Check device + capabilities at runtime for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md). +- **Hardware Permissions:** To request hardware permissions like the microphone and camera, see the [Request hardware permissions for AI + glasses](references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md). +- **Hardware Access:** To use the glasses camera, sensors, or access the phone's hardware, see the [Use a projected context to access AI glasses + hardware](references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md). + +## 2. Minimize and translate the UI + +- For Display AI Glasses, build UI using components from the Jetpack Compose Glimmer framework. +- Use depth to communicate element priority and hierarchy. +- Design from the bottom up, trying to minimize how much of the real world you cover. Always bottom align UI to the glasses display. +- **One Thing at a Time:** Prioritize the user's awareness of the real world. Show only one primary piece of information at a time (for example, using a `Stack`) to minimize obstruction of the user's field of view. Avoid multiple simultaneous cards. +- **Color Contrast:** Ensure at least a 70% tone difference between foreground and background using the HCT color space. For calculation metrics, use [references/material-hct-source.md](references/material-hct-source.md). + +## 3. Map input controls + +- Map app interactions, such as tap and swipe, to the available hardware controls on the glasses, such as the touchpad. +- Inputs are more 1-dimensional; users typically make one control input at a time. +- Avoid nesting scrolling controls. +- Jetpack Compose Glimmer components are designed to work with standard input methods, such as a tap or swipe on the AI glasses' touchpad. +- Use System Back to dismiss temporary states or detailed views. +- To add input, focus, tap, swipe to your Glasses UI, follow [Focus in Jetpack + Compose Glimmer](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md). + - For a detailed breakdown of hardware inputs, see [Hardware Controls for + Display AI Glasses](references/android/design/ui/ai-glasses/guides/interaction/inputs.md) + +## 4. Build with Jetpack Compose Glimmer + +Jetpack Compose Glimmer is the UI toolkit for building augmented experiences on +Display AI Glasses. + +### Key Features + +- **Glimmer Theming:** A simplified glasses-specific theme for optimal visibility. +- **Pre-compatible Input:** Supports tap and swipe by default. +- **Pre-built Components:** Provides optimized composables like `Card`, `ListItem`, `Button`, etc. + +### Focus Management in Glimmer + +- Jetpack Compose Glimmer uses a one-dimensional focus search. +- Focus movement is continuous for scrollable containers and discrete for elements like a row of buttons. +- To enable the system to automatically set initial focus, you must set the `isInitialFocusOnFocusableAvailable` flag to `true` in your activity's `onCreate` method. For more information on how to implement, see [Focus in + Jetpack Compose Glimmer](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md). + +### Implementing Glimmer Styles + +Glimmer styles are accessed through the `GlimmerTheme` object. Use +[references/glimmertheme-source.md](references/glimmertheme-source.md) for reference. + +| Category | Token | Value / Role | +|---|---|---| +| **Color** | primary | #9BBFFF (Focal color) | +| **Color** | secondary | #4C88E9 (Focal color) | +| **Color** | surface | #262626 (Transparent base - renders as transparent) | +| **Color** | outline | #606460 (3.dp border color) | +| **Shape** | Standard | `RoundedCornerShape(36.dp)` | +| **Shape** | Small | `RoundedCornerShape(12.dp)` | + +#### Typography Scale (Google Sans Flex) + +**Strict default:** When creating Glimmer UI you must use Google Sans Flex +unless a custom brand typeface is explicitly specified. +**Variable Font Settings:** As Google Sans Flex is a variable font, you must +configure the following axes: + +- **Roundness (`ROND`):** Always set to `100f` for the signature rounded appearance. +- **Width (`wdth`):** Set to `100f`. +- **Optical Size (`opsz`):** Set to `9f`. +- **Weight (`wght`):** Use specific values for different roles (Title: `725f`, Body: `520f`, Caption: `650f`). + +| Style Name | Size / Line-Height | Weight Axis | Width | Roundness | Optical size | +|---|---|---|---|---|---| +| **Title Large** | 30.sp / 36.sp | 725 | 100 | 100 | 9 | +| **Title Medium** | 24.sp / 32.sp | 725 | 100 | 100 | 9 | +| **Title Small** | 20.sp / 28.sp | 725 | 100 | 100 | 9 | +| **Body Large** | 30.sp / 36.sp | 520 | 100 | 100 | 9 | +| **Body Medium** | 24.sp / 32.sp | 520 | 100 | 100 | 9 | +| **Body Small** | 20.sp / 28.sp | 520 | 100 | 100 | 9 | +| **Caption** | 18.sp / 28.sp | 650 | 100 | 100 | 9 | + +#### Depth Levels + +Simulate depth on AI glasses using shadows to establish a sense of hierarchy +through varying levels of emphasis. The Jetpack Compose Glimmer controls use +`DepthEffect` with 5 preset `DepthEffectLevels`. Use +[references/deptheffect-source.md](references/deptheffect-source.md) and +[references/deptheffectlevels-source.md](references/deptheffectlevels-source.md) for reference. + +Some examples: + +| Level | Usage | +|---|---| +| **level1** | Standard rest state for cards and persistent background UI. | +| **level2** | Standard focus/pressed state for buttons and interactive cards. | +| **ExtraSmall** | 4.dp | + +### Implementing Jetpack Compose Glimmer Components + +#### Cards + +Cards are a fundamental surface-based container in Glimmer used to group related +content, such as text, images, icons and actions into a single focal point. They +establish a clear depth plane (Z-axis) in the Glasses environment, providing a +stable background for text, images, and icons. Never embed a card within a List +Item. + +##### Core Implementation Logic + +- **Surface Hierarchy:** Cards are designed to sit on a base surface or within a list. They provide an automatic visual feedback when focused if an `onClick` lambda is provided. +- **Interactivity:** + - **IF** the entire Card serves as a single trigger (e.g., a media item or a setting): **THEN** provide an `onClick` lambda to enable focus effects. + - **ELSE:** Leave `onClick` as `null` if the Card contains multiple internal interactive elements (like separate buttons or switches) to avoid focus contention. + +##### Technical Documentation Links + +If you are creating a Glimmer Card component, read the: + +- **Developer Guidance:** [Jetpack Compose Glimmer: Cards](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md) +- **API Source Code:** Use [references/card-source.md](references/card-source.md). +- **Implementation Samples:** Use [references/card-samples-source.md](references/card-samples-source.md). (Basic and Interactive Card usage) + +#### Buttons + +Buttons are the primary triggers for discrete actions in Glimmer. They are +specifically optimized for the AI Glasses focus model, where a focus highlight +is added when focus is moved to the button using the touchpad or other methods. + +##### Core Implementation Logic + +- There are two types of buttons to choose from in Glimmer: + - **Standard buttons** (Required text label with optional leading or trailing icons). Use this when there is more space in the UI, or when the meaning of the button isn't clear without text. + - **Icon buttons** (icon only). Only use icon buttons when the icon is clearly understandable. +- Both icon and standard buttons have default and toggle variants: + - Default (use for single actions like "buy" or "navigate") + - Toggle (use for things with selected states like mute buttons) + +##### Technical Documentation Links + +If you are creating a Glimmer Button component, read the: + +- **Developer Guidance:** [Jetpack Compose Glimmer: Buttons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md) +- **API Source Code:** Use [references/button-source.md](references/button-source.md). +- **Implementation Samples:** Use [references/button-samples-source.md](references/button-samples-source.md). + +#### Title Chips + +Chips are a pill-shaped, specialized labeling component designed to sit above a +`Card` or a group of content to provide a title. Use title chips to display +concise information like a short title, a name, or a status. + +##### Guidelines and usage + +- **Spatial Spacing:** When using a standalone `TitleChip` above content, you must use `TitleChipDefaults.AssociatedContentSpacing` (8.dp) to maintain the visual hierarchy. +- **Interactivity:** Title chips are purely for informational purposes, they cannot be targeted or activated for navigation. +- **Layout** Always center text in a title chip. Never let the title chip go to two lines, and truncate extra words. Keep the label to three words or less. + +##### Technical Documentation Links + +If you are creating a Glimmer Title Chip component, read the: + +- **Developer Guidance:** [Jetpack Compose Glimmer: Title Chips](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md) +- **API Source Code:** Use [references/titlechip-source.md](references/titlechip-source.md). +- **Implementation Samples:** Use [references/titlechip-samples-source.md](references/titlechip-samples-source.md). (Title Chips above group content) + +#### Icons + +Icons are visual symbols used to represent concepts, actions, or status in a +concise way. In Glimmer, icons and icon buttons are optimized for the XR +environment, providing clear visibility on additive displays and gaze-responsive +feedback for interactive elements. + +##### Guidelines and usage + +- **Sizing:** Use predefined `IconSizes` (e.g., Standard, Large) to ensure icons remain legible and meet the minimum touch target requirements for the XR environment. +- **Interactivity:** + - **If** an icon serves as a trigger: **Then** you must use an `IconButton` to provide the automatic visual feedback and required tap-target padding. + - **ELSE:** Use a standalone `Icon` for non-interactive indicators or status symbols. +- **Contrast:** **NEVER** use pure black (#000000) for icon tints; always use themed colors or standard Glimmer content colors to ensure the icon is visible against dark or real-world backgrounds. +- **Icon Library** The default icon library is [Material Symbols](https://fonts.google.com/icons?icon.style=Rounded) with 600 weight, Rounded, Unfilled, unless otherwise specified. + +##### Technical Documentation Links + +If you are creating a Glimmer Icon component, read the: + +- **Developer Guidance:** [Jetpack Compose Glimmer: Icons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md) +- **API Source Code (Icon):** Use [references/icon-source.md](references/icon-source.md). +- **API Source Code (IconButton):** Use [references/iconbutton-source.md](references/iconbutton-source.md). +- **API Source Code (IconSizes):** Use [references/iconsizes-source.md](references/iconsizes-source.md). + +#### Lists + +Lists are containers that allow you to navigate between and see multiple items +on glasses. If your use case works with only seeing one item in the list at a +time, use a Glimmer Stack. Use lists with a Title Chip when the list +items are similar in type. Also use a Glimmer Stack if the items are of +different types. + +##### Guidelines and usage + +- **ListItem Slots:** Use the `ListItem` composable for rows. It provides predefined slots. Use [references/listitem-source.md](references/listitem-source.md) for reference. +- **Visual Consistency:** When building lists of similar items, always use a consistent background color (typically `GlimmerTheme.colors.surface`) and corner radius (standard 36.dp) for every item. Don't vary these unless you are visually grouping different *types* of content. +- **Integrated Title Chips:** Glimmer Lists support integrated title chips. **IF** you need a section header within a list: **THEN** enable the integrated title chip rather than adding a standalone `TitleChip` to maintain spatial consistency. +- **Vertical Arrangement:** ALWAYS use `verticalArrangement = + Arrangement.spacedBy(20.dp)` for `VerticalList` to ensure visual separation between items on the glasses display. +- Be sure to use the default 20 dp spacing between list items unless otherwise specified. + +##### Technical Documentation Links + +If you are creating a Glimmer List component, read the: + +- **API Source Code (List):** Use [references/list-source.md](references/list-source.md). +- **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). +- **API Source Code (ListState):** Use [references/liststate-source.md](references/liststate-source.md). + +#### Stacks + +A stack is a collapsed list that only displays one piece of content at a time, +in a stacked visual, such as a card. If it is useful to show more than one item +at a time, use the Glimmer List control. Don't use a title chip with a stack. +If the items are of different types use a stack to show them. If the items are +of the same type, use a list. + +##### Guidelines and usage + +**Layout tips** + +- Stacks accept variable height items. +- Align your stack control with the bottom of the display. +- The stack control must be 66 dp taller than the tallest item in the stack, allowing room for the scrim and minimizing movement when navigating between items. +- **Clipping and Layering:** To ensure that items behind the top item are correctly clipped and hidden, you **must** apply the `.itemDecoration(CardDefaults.shape)` modifier to the `Card` (or relevant container) inside every `item` block. +- **Depth Shadows:** To maintain spatial separation and element priority, utilize Glimmer's `ShadowScope`, `DepthEffect`, or prescribed `DepthEffectLevels` for depth plane scaling. + +##### Technical Documentation Links + +If you are creating a Glimmer Stack component, read the: + +- **API Source Code (Stack):** Use [references/stack-source.md](references/stack-source.md). +- **API Source Code (StackState):** Use [references/stackstate-source.md](references/stackstate-source.md). +- **API Source Code (StackItemScope):** Use [references/stackitemscope-source.md](references/stackitemscope-source.md). + +#### Text + +In Jetpack Compose Glimmer, the [`Text`](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md) component builds off the Compose +text component, and lets you set various text properties. Be sure to choose a +style from the `GlimmerTheme` for your text. Modify the theme for your +application if you want custom typography. + +#### Surface + +`Surface` is a fundamental building block in Glimmer. Use +[references/surface-source.md](references/surface-source.md) for reference. + +A surface represents a distinct visual area or 'physical' boundary for +components such as buttons and cards. Use it if you need to build a custom +component. + +## 5. Integrate with system UI + +- For a detailed breakdown of notifications on AI Glasses, see [Understand + notification behavior for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md) and learn how to [Start a glasses + activity on display AI glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md new file mode 100644 index 0000000..8dd8dd6 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md @@ -0,0 +1,67 @@ +Glasses have multiple sources and types of interaction inputs, including +hardware and touch, physical gesture, and voice. + +## Hardware controls + +Glasses controls can vary depending on device model, but most will include a +camera button, touchpad, display button, and power switch or button. Hardware +controls have default interactions, and some can be remapped for your app. +Consider that inputs for glasses are more 1-dimensional, where users can make +one control input at a time, compared to touchscreen inputs. The glasses +hardware controls have default interaction mapping, some of which are handled by +the app, and others that are handled by the system. + +**The system will handle the following inputs:** + +- Camera button single press to take photos and camera button hold to take videos +- Swiping with two fingers on the touchpad for volume +- Touch \& hold on the touchpad and the wake word on the microphone will launch Gemini +- System interprets swipe down on display glasses as system back. + +**Your app has access to the glasses:** + +- Microphone +- Point-of-View (POV) camera +- Inertial Measurement Unit (IMU) +- Camera button double-press + +![Design elements should be anchored to the bottom of the +frame.](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_ixd_inputs_hardware.png) + +### AI Glasses + + +| Input | Gesture | Interaction affect | +|---|---|---| +| Touchpad (1) | Tap | Play / Pause / Confirm | +| | Swipe | Next/Previous/Dismiss | +| | Swipe down | N/A | +| | Touch \& hold | Invoke AI | +| | 2-finger swipe | Volume | +| Display button (2) | Press | N/A | +| Camera button (3) | Press | Photo / Video end | +| | Touch \& hold | Video start | +| | Double press | Camera | + +
+ +### AI Glasses with Display + + +| Input | Gesture | Interaction affect | +|---|---|---| +| Touchpad (1) | Tap | Play / Pause / Confirm | +| | Swipe | UI Navigation | +| | Swipe down | Back | +| | Touch \& hold | Invoke AI | +| | 2-finger swipe | Volume | +| Display button (2) | Press | Wake/Sleep | +| Camera button (3) | Press | Photo / Video end | +| | Touch \& hold | Video start | +| | Double press | Camera | + +
+ +![Design elements should be anchored to the bottom of the +frame.](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_ixd_inputs_focus.png) On +display focus states have a visual affordances. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md new file mode 100644 index 0000000..67de706 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md @@ -0,0 +1,221 @@ +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +After you've [requested and been granted the necessary permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions), your app +can access the AI glasses hardware. The key to accessing the glasses' hardware +(instead of the phone's hardware), is to use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context). + +There are two primary ways to get a projected context, depending on where your +code is executing: + +## Get a projected context if your code is running in an AI glasses activity + +If your app's code is running from within your AI glasses activity, its own +activity context is already a projected context. In this scenario, calls made +within that activity can already access the glasses' hardware. + +## Get a projected context for code running in a phone app component + +If a part of your app outside of your AI glasses activity (such as a phone +activity or a service) needs to access the glasses' hardware, it must explicitly +obtain a projected context. To do this, use the +[`createProjectedDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) method: + + +```kotlin +@OptIn(ExperimentalProjectedApi::class) +private fun getGlassesContext(context: Context): Context? { + return try { + // From a phone Activity or Service, get a context for the AI glasses. + ProjectedContext.createProjectedDeviceContext(context) + } catch (e: IllegalStateException) { + Log.e(TAG, "Failed to create projected device context", e) + null + } +} +``` + +
+ +### Check for validity + +Wrap the [`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) call within the +[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)). While this method returns +`true`, the projected context remains valid to the connected device, and your +phone app activity or service (such as a `CameraManager`) can access the +AI glasses hardware. + +### Clean up on disconnect + +The projected context is tied to the lifecycle of the connected device, so it is +destroyed when the device disconnects. When the device disconnects, +[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)) returns `false`. Your app +should listen for this change and clean up any system services (such as a +`CameraManager`) or resources that your app created using that projected +context. + +### Re-initialize on reconnect + +When the AI glasses device reconnects, your app can obtain another projected +context instance using [`createProjectedDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)), and then +re-initialize any system services or resources using the new projected context. + +## Access audio using bluetooth + +Currently, AI glasses connect to your phone as a standard Bluetooth audio +device. Both the headset and the A2DP (Advanced Audio Distribution Profile) +[profiles](https://developer.android.com/develop/connectivity/bluetooth/profiles) are supported. Using this approach lets any Android app that +supports audio input or output work on glasses, even if they were not +purposefully built to support glasses. In some cases, using Bluetooth might work +better for your app's use case as an alternative to accessing the glasses' +hardware using a projected context. + +As with any standard Bluetooth audio device, the permission to grant the +[`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio) permission is controlled by the phone and not the glasses. + +## Capture an image with the AI glasses' camera + +To capture an image with the AI glasses' camera, set up and bind the CameraX's +[`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture) [use case](https://developer.android.com/media/camera/camerax#ease-of-use) to the glasses' camera using the correct +context for your app: + + +```kotlin +private fun startCameraOnGlasses(activity: ComponentActivity) { + // 1. Get the CameraProvider using the projected context. + // When using the projected context, DEFAULT_BACK_CAMERA maps to the AI glasses' camera. + val projectedContext = try { + ProjectedContext.createProjectedDeviceContext(activity) + } catch (e: IllegalStateException) { + Log.e(TAG, "AI Glasses context could not be created", e) + return + } + + val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext) + + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + // 2. Check for the presence of a camera. + if (!cameraProvider.hasCamera(cameraSelector)) { + Log.w(TAG, "The selected camera is not available.") + return@addListener + } + + // 3. Query supported streaming resolutions using Camera2 Interop. + val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) + val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo) + val cameraCharacteristics = camera2CameraInfo.getCameraCharacteristic( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP + ) + + // 4. Define the resolution strategy. + val targetResolution = Size(1920, 1080) + val resolutionStrategy = ResolutionStrategy( + targetResolution, + ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER + ) + val resolutionSelector = ResolutionSelector.Builder() + .setResolutionStrategy(resolutionStrategy) + .build() + + // 5. If you have other continuous use cases bound, such as Preview or ImageAnalysis, + // you can use Camera2 Interop's CaptureRequestOptions to set the FPS + val fpsRange = Range(30, 60) + val captureRequestOptions = CaptureRequestOptions.Builder() + .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange) + .build() + + // 6. Initialize the ImageCapture use case with options. + val imageCapture = ImageCapture.Builder() + // Optional: Configure resolution, format, etc. + .setResolutionSelector(resolutionSelector) + .build() + + try { + // Unbind use cases before rebinding. + cameraProvider.unbindAll() + + // Bind use cases to camera using the Activity as the LifecycleOwner. + cameraProvider.bindToLifecycle( + activity, + cameraSelector, + imageCapture + ) + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + }, ContextCompat.getMainExecutor(activity)) +} +``` + +
+ +### Key points about the code + +- Obtains an instance of the [`ProcessCameraProvider`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider) using the [**projected device context**](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware-projected-context#phone-activity-service). +- Within the projected context's scope, the AI glasses' primary, outward-pointing camera maps to the `DEFAULT_BACK_CAMERA` when selecting a camera. +- A pre-binding check uses [`cameraProvider.hasCamera(cameraSelector)`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider#hasCamera(androidx.camera.core.CameraSelector)) to verify that the selected camera is available on the device before proceeding. +- Uses **Camera2 Interop** with [`Camera2CameraInfo`](https://developer.android.com/reference/kotlin/androidx/camera/camera2/interop/Camera2CameraInfo) to read the underlying [`CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP`](https://developer.android.com/reference/kotlin/android/hardware/camera2/CameraCharacteristics#scaler_stream_configuration_map), which can be useful for advanced checks on supported resolutions. +- A custom [`ResolutionSelector`](https://developer.android.com/reference/kotlin/androidx/camera/core/resolutionselector/ResolutionSelector) is built to precisely control the output image resolution for [`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture). +- Creates an `ImageCapture` use case that is configured with a custom `ResolutionSelector`. +- Binds the `ImageCapture` use case to the activity's lifecycle. This automatically manages the opening and closing of the camera based on the activity's state (for example, stopping the camera when the activity is paused). + +After the AI glasses' camera is set up, you can capture an image with the +CameraX's `ImageCapture` class. Refer to the CameraX's documentation to learn +about using [`takePicture()`](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture) to [capture an image](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture). + +### Capture a video with the AI glasses' camera + +To capture a video instead of an image with the AI glasses' camera, replace the +`ImageCapture` components with the corresponding [`VideoCapture`](https://developer.android.com/reference/kotlin/androidx/camera/video/VideoCapture) components +and modify the capture execution logic. + +The main changes involve using a different use case, creating a different output +file, and initiating the capture using the appropriate video recording method. +For more information about the `VideoCapture` API and how to use it, see the +[CameraX's video capture documentation](https://developer.android.com/media/camera/camerax/video-capture#using-videocapture-api). + +> [!IMPORTANT] +> **Important:** Battery power and thermal dissipation on AI glasses devices is often limited. When developing for glasses, pay close attention to the requested **output format** and **resolution**, as these can severely impact system power and temperature. + +The following table shows the recommended resolution and frame rate depending on +your app's use case: + +| Use case | Resolution | Frame rate | +|---|---|---| +| Video Communication | 1280 x 720 | 15 FPS | +| Computer Vision | 640 x 480 | 10 FPS | +| AI Video Streaming | 640 x 480 | 1 FPS | + +## Access a phone's hardware from an AI glasses activity + +An AI glasses activity can also access the phone's hardware (such as the camera +or microphone) by using [`createHostDeviceContext(context)`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the host +device's (phone) context: + + +```kotlin +@OptIn(ExperimentalProjectedApi::class) +private fun getPhoneContext(activity: ComponentActivity): Context? { + return try { + // From an AI glasses Activity, get a context for the phone. + ProjectedContext.createHostDeviceContext(activity) + } catch (e: IllegalStateException) { + Log.e(TAG, "Failed to create host device context", e) + null + } +} +``` + +
+ +When accessing hardware or resources that are specific to the host device +(phone) in a hybrid app (an app containing both mobile and AI glasses +experiences), you must explicitly select the correct context to make sure your +app can access the correct hardware: + +- Use the [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) context from the phone `Activity` or the [`ProjectedContext.createHostDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the phone's context. +- Don't use [`getApplicationContext()`](https://developer.android.com/reference/kotlin/android/content/Context#getapplicationcontext) because the application context can incorrectly return the AI glasses' context if a glasses activity was the most-recently-launched component. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md new file mode 100644 index 0000000..eea9a07 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md @@ -0,0 +1,47 @@ +Different types of AI glasses have different capabilities. After [planning how +you'll support different types of AI devices](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types), you can check for device +capabilities at runtime to provide the best experience for a user's device. + +## Check whether a device has a display + +Some AI glasses have a display where your app can show [UIs built with Jetpack +Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer). The following example shows how to check whether a glasses +device has a display: + + +```kotlin +// Check device capabilities +val projectedDeviceController = ProjectedDeviceController.create(this@GlassesMainActivity) +isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI) +``` + +
+ +## React to display state changes + +On AI glasses with a display, the display can time out or the user can turn off +the display. To design activities that run whether the display is on or off, +use [`addPresentationModeChangedListener`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDisplayController#addPresentationModeChangedListener(java.util.concurrent.Executor,java.util.function.Consumer)) to be notified when the display +state changes. You can tune your activity for the appropriate amount of +audio information depending on display state. + + ProjectedDisplayController.create(activity).addPresentationModeChangedListener { + presentationModeFlags -> + + val areVisualsOff = !presentationModeFlags.hasPresentationMode(VISUALS_ON) + } + +> [!NOTE] +> **Note:** Monitor display state changes for the AI glasses directly within the [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle). + +## Keep the display on + +On AI glasses with a display, you can request that the system keep the screen on +and prevent the screen from timing out using [`addLayoutParamsFlags`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDisplayController#addLayoutParamsFlags(kotlin.Int)). + +> [!NOTE] +> **Note:** Keeping the device's screen on can drain the battery quickly. Ordinarily, you should let the device turn the screen off if the user isn't interacting with it. If you do need to keep the screen on, do so for as short a time as possible. + + var projectedDisplayController = ProjectedDisplayController.create(activity) + + projectedDisplayController.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md new file mode 100644 index 0000000..9c4760b --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md @@ -0,0 +1,278 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +The AI glasses experience is built on the existing Android [`Activity` framework +API](https://developer.android.com/guide/components/activities/intro-activities) and [includes additional concepts](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle) to support the unique aspects of +AI glasses. Unlike XR headsets that run a full APK on the device, AI glasses use +a dedicated activity that runs within your phone's existing app. This activity +is projected from the host device to the AI glasses. + +To create your app's AI glasses experience, you extend your existing phone app +by creating a new projected [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) for AI glasses. This activity serves +as the main launch entry point for your app on AI glasses. This approach +simplifies development because you can share and reuse business logic between +your phone and AI glasses experiences. + +## Version compatibility + +Check the [Android SDK compatibility requirements](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#compatibility) for the Jetpack XR SDK. + +### Dependencies + +Add the following [library dependencies for AI glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#augmented): + +### Groovy + +```groovy +dependencies { + implementation "androidx.xr.runtime:runtime:1.0.0-alpha12" + implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha10" + implementation "androidx.xr.projected:projected:1.0.0-alpha06" + implementation "androidx.xr.arcore:arcore:1.0.0-alpha12" +} +``` + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.xr.runtime:runtime:1.0.0-alpha12") + implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha10") + implementation("androidx.xr.projected:projected:1.0.0-alpha06") + implementation("androidx.xr.arcore:arcore:1.0.0-alpha12") +} +``` + +## Declare your activity in your app's manifest + +Just like other types of activities, you need to declare your activity in your +app's manifest file for the system to see and run it. + + + + + + + + + +### Key points about the code + +- Specifies `xr_projected` for the `android:requiredDisplayCategory` attribute to tell the system that this activity should use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context) to access hardware from a connected device. + +## Create your activity + +Next, you'll create a small activity that can display something on the AI +glasses whenever the display is turned on. + + +```kotlin +@OptIn(ExperimentalProjectedApi::class) +class GlassesMainActivity : ComponentActivity() { + + private var displayController: ProjectedDisplayController? = null + private var isVisualUiSupported by mutableStateOf(false) + private var areVisualsOn by mutableStateOf(true) + private var isPermissionDenied by mutableStateOf(false) + + // Register the permissions launcher using the ProjectedPermissionsResultContract. + private val requestPermissionLauncher: ActivityResultLauncher> = + registerForActivityResult(ProjectedPermissionsResultContract()) { results -> + if (results[Manifest.permission.CAMERA] == true) { + isPermissionDenied = false + initializeGlassesFeatures() + } else { + // Handle permission denial. + isPermissionDenied = true + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + displayController?.close() + displayController = null + } + }) + + if (hasCameraPermission()) { + initializeGlassesFeatures() + } else { + requestHardwarePermissions() + } + + setContent { + GlimmerTheme { + HomeScreen( + areVisualsOn = areVisualsOn, + isVisualUiSupported = isVisualUiSupported, + isPermissionDenied = isPermissionDenied, + onRetryPermission = { requestHardwarePermissions() }, + onClose = { finish() } + ) + } + } + } + + private fun initializeGlassesFeatures() { + lifecycleScope.launch { + // Check device capabilities + val projectedDeviceController = ProjectedDeviceController.create(this@GlassesMainActivity) + isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI) + + val controller = ProjectedDisplayController.create(this@GlassesMainActivity) + displayController = controller + val observer = GlassesLifecycleObserver( + context = this@GlassesMainActivity, + controller = controller, + onVisualsChanged = { visualsOn -> areVisualsOn = visualsOn } + ) + lifecycle.addObserver(observer) + } + } + + private fun hasCameraPermission(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED + } + + private fun requestHardwarePermissions() { + val params = ProjectedPermissionsRequestParams( + permissions = listOf(Manifest.permission.CAMERA), + rationale = "Camera access is required to overlay digital content on your physical environment." + ) + requestPermissionLauncher.launch(listOf(params)) + } +} +``` + +
+ +### Key points about the code + +- Opts in to using [opt-in APIs](https://developer.android.com/reference/kotlin/androidx/annotation/RequiresOptIn) from the [Jetpack Projected](https://developer.android.com/jetpack/androidx/releases/xr-projected) library. +- `GlassesMainActivity` extends [`ComponentActivity`](https://developer.android.com/reference/kotlin/androidx/activity/ComponentActivity), just as you would expect in mobile development. +- Because not all AI glasses have a display, checks whether the device has a display using [`ProjectedDeviceController`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDeviceController). +- The `setContent` block within the `onCreate` function defines the root of the Composable UI tree for the activity. You'll implement the `HomeScreen` composable using [Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer). +- Initializes the UI during the activity's [`onCreate`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method (see [projected activity lifecycle](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle)). +- To prepare for camera-related features that [access the glasses hardware](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware-projected-context), [requests hardware permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions) by registering a permissions launcher, defining the `hasCameraPermission` and `requestHardwarePermissions` functions, and checking whether permissions have been granted before calling `initializeGlassesFeatures`. + +## Implement the composable + +The activity that you created references a `HomeScreen` composable function that +you need to implement. The following code uses [Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer) to +define a composable that can display some text on the AI glasses' display: + + +```kotlin +@Composable +fun HomeScreen( + areVisualsOn: Boolean, + isVisualUiSupported: Boolean, + isPermissionDenied: Boolean, + onRetryPermission: () -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .surface(focusable = false) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (isPermissionDenied) { + Card( + title = { Text("Permission Required") }, + action = { Button(onClick = onClose) { Text("Exit") } } + ) { + Text("Camera access is needed to use AI glasses features.") + Button(onClick = onRetryPermission) { Text("Retry") } + } + } else if (isVisualUiSupported) { + Card( + title = { Text("Android XR") }, + action = { + Button(onClick = onClose) { + Text("Close") + } + } + ) { + if (areVisualsOn) { + Text("Hello, AI Glasses!") + } else { + Text("Display is off. Audio guidance active.") + } + } + } else { + Text("Audio Guidance Mode Active") + } + } +} +``` + +
+ +### Key points about the code + +- As you defined in your activity earlier, the `HomeScreen` function includes the composable content that the user sees when the AI glasses' display is on. +- The Jetpack Compose Glimmer [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.text.TextStyle)) component displays the text "Hello, AI Glasses!" to the glasses' display. +- The Jetpack Compose Glimmer `Button` closes the activity by calling `finish()` through `onClose` in the AI glasses activity. + +> [!NOTE] +> **Note:** For an example of developing for a displayless experience, see [Handle +> audio input using Automatic Speech Recognition](https://developer.android.com/develop/xr/jetpack-xr-sdk/asr) and [Handle audio input +> using Text to Speech](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts). + +## Check whether AI glasses are connected + +To determine whether a user's AI glasses are connected to their phone before +launching your activity, use the +[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)) method. This method +returns a `Flow` that your app can observe to get real-time updates on +the connection status. + +## Start your activity + +> [!WARNING] +> **Preview:** There is a known rendering issue that causes the AI glasses' display to briefly flash white when a projected activity starts. + +Now that you've created a basic activity, you can launch it onto your glasses. +To access the glasses' hardware, your app must start your activity with specific +options that tell the system to use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context), as shown in the +following code: + + +```kotlin +val options = ProjectedContext.createProjectedActivityOptions(context) +val intent = Intent(context, GlassesMainActivity::class.java) +context.startActivity(intent, options.toBundle()) +``` + +
+ +The [`createProjectedActivityOptions`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedActivityOptions(android.content.Context)) method in [`ProjectedContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext) +generates the necessary options to start your activity in a projected context. +The `context` parameter can be a context from the phone or the glasses device. + +> [!WARNING] +> **Preview:** When your app launches a projected activity, it doesn't automatically turn on the AI glasses' display. We're planning to add this capability in the future. + +## Next steps + +Now that you've created your first activity for AI glasses, explore other ways +that you can extend its functionality: + +- [Handle audio output using Text to Speech](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts) +- [Handle audio input using Automatic Speech Recognition](https://developer.android.com/develop/xr/jetpack-xr-sdk/asr) +- [Build UI with Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer) +- [Access AI glasses' hardware](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware) \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md new file mode 100644 index 0000000..77d01dd --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md @@ -0,0 +1,130 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +AI glasses use the standard Android notification framework, so you can notify +users across different form factors using the same notification APIs. To promote +maximum compatibility and minimize development overhead, use the Android +[`NotificationCompat`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat) APIs. Android parses incoming notifications and adapts +their presentation based on the capabilities of the device. + +By following the [best practices for notifications](https://developer.android.com/develop/ui/views/notifications), your existing phone +notifications can be bridged to AI glasses with little to no additional +configuration. Read the information in the following sections to understand how +the system adapts notifications for AI glasses. + +## How the system determines whether to bridge a notification to AI glasses + +Android uses the incoming notification's [`Notification.Style`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Style) together with +several other criteria to determine whether or not to bridge the notification to +the user's AI glasses. + +### Supported notification styles + +AI glasses support a subset of Android `Notification.Style` classes. The +following notification styles are fully rendered on display AI glasses: + +- [Standard style (`NotificationCompat.Style`)](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Style) + + ![Standard style + notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/standard-notification.png) +- [`MessagingStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.MessagingStyle) + + ![MessagingStyle + notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/messagingstyle-notification.png) +- [`BigPictureStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.BigPictureStyle) + + ![BigPictureStyle + notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/bigpicturestyle-notification.png) +- [`ProgressStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.ProgressStyle) + +- [`MediaStyle`](https://developer.android.com/reference/kotlin/androidx/media/app/NotificationCompat.MediaStyle) + +- [`CallStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.CallStyle) (only when the notification [qualifies as a live + update](https://developer.android.com/develop/ui/views/notifications/live-update)) + +Other notification styles (such as [`InboxStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.InboxStyle)) aren't fully parsed and +rendered. For these styles, style-specific fields aren't rendered (such as the +[summary text](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.InboxStyle#setSummaryText(java.lang.CharSequence)) for `InboxStyle`). Instead, the system reverts to the +standard style and renders only common fields such as the [content title](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Builder#setContentTitle(java.lang.CharSequence)). + +> [!NOTE] +> **Note:** Custom notifications with `RemoteViews` aren't bridged to AI glasses. + +### Other required criteria for bridging + +Besides the notification's style, a notification must also meet the following +criteria to be bridged to AI glasses: + +1. The notification isn't subject to **any** of the following user-controlled + settings that would prevent its delivery: + + - **Glasses companion app settings**: + + - App-level toggle: By default, **app notifications are toggled off** + in the Glasses app to help users purposefully + decide which notifications are bridged to their AI glasses. + + This default behavior lets a user leave notifications + enabled on their phone for a certain app, but disable them for that + app on their AI glasses. To help a user decide whether to enable + notifications for your app in the Glasses app, + explain how notifications would improve their experience with your + app. + - **System-level notification settings on the user's phone**: + + - App-level toggle: If a user disables notifications entirely for an + app on the phone, no notifications for that app are bridged. + + - Notification channel settings: If a user disables notifications for + an [app-defined notification channel](https://developer.android.com/develop/ui/views/notifications/channels), no notifications for + that channel are bridged. + + - **System-level Do Not Disturb (DND) settings on the user's phone**: AI + glasses use the phone's DND settings. If the user's phone is in DND + mode, notifications are also suppressed on the user's glasses. + +2. The notification is assigned to a channel with [`IMPORTANCE_HIGH`](https://developer.android.com/reference/kotlin/android/app/NotificationManager#importance_high) or + [`IMPORTANCE_MAX`](https://developer.android.com/reference/kotlin/android/app/NotificationManager#importance_max). + +3. The [notification's title](https://developer.android.com/reference/kotlin/android/app/Notification#extra_title) isn't `null` or empty. + +4. The notification isn't marked with [`FLAG_LOCAL_ONLY`](https://developer.android.com/reference/kotlin/android/app/Notification#flag_local_only). If this flag is + set, the notification is restricted to the primary device. + +5. The notification isn't an [ongoing notification](https://developer.android.com/reference/kotlin/android/app/Notification.Builder#setongoing), such as a persistent + background task, unless it [qualifies as a Live Update notification](https://developer.android.com/develop/ui/views/notifications/live-update). + +## How Live Update notifications are bridged to AI glasses + +[Live Update notifications](https://developer.android.com/develop/ui/views/notifications/live-update) are a specialized class of notifications designed +for ongoing, user-initiated activities that require real-time monitoring, such +as rideshare ETAs, turn-by-turn navigation, or active calls. Unlike regular +notifications, live updates remain active to provide a continuous stream of +information that are surfaced prominently across the system UI. + +For display AI glasses, live updates are rendered in two primary locations: + +- **Home screen** : Live Update notifications appear on the Home canvas as [cards](https://developer.android.com/design/ui/ai-glasses/guides/components/cards). If multiple live updates are active, the system uses a [stack](https://developer.android.com/design/ui/ai-glasses/guides/components/stack) instead. +- **System bar** : When the user is inside another app or experience, live updates appear as status chips in the [system bar](https://developer.android.com/design/ui/ai-glasses/guides/surfaces/sysui#system-bar). These chips appear briefly whenever a status change occurs. If the display is asleep, a status chip automatically wakes the screen to signal a status change, so the user stays informed without manual interaction. + +If a live update notification uses a [supported notification style](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior#notification-styles) for AI +glasses, it is fully parsed and rendered. Live Updates notifications that use +other notification styles (such as [`BigTextStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.BigTextStyle)) aren't fully parsed, +and the system adapts them to a standard style notification instead. + +## Available notification actions for display AI glasses + +On display AI glasses, incoming notifications appear as [heads-up +notifications](https://developer.android.com/develop/ui/views/notifications#Heads-up) (HUNs). For `MessagingStyle` notifications, your app can use +a [direct reply](https://developer.android.com/develop/ui/views/notifications/build-notification#reply-action) action. Users can tap to expand for more details and reply. +For all other notification styles, the only available option is the +system-provided clear action. + +When using direct reply, users can reply with the voice or select from a smart +reply list by scrolling forward. For smart replies, you can use our [on-device +AI](https://developers.google.com/ml-kit/language/smart-reply/android) +to suggest short, relevant replies. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md new file mode 100644 index 0000000..45fe05c --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md @@ -0,0 +1,77 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +You can specify a different [`PendingIntent`](https://developer.android.com/reference/kotlin/android/app/PendingIntent) that is invoked when a +notification is tapped *on the glasses display* . This `PendingIntent` is +distinct from the default intent set for the phone using +[`setContentIntent`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Builder#setContentIntent(android.app.PendingIntent)). For example, when the user taps on the notification on +the display AI glasses, a specific glasses activity launches on the display AI +glasses. + +To add glasses-specific behaviors, use [`ProjectedExtender`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.ProjectedExtender). This API lets +you to customize the notification's behavior on the glasses without affecting +how it appears on the phone. + +### Kotlin + + // Intent to fire when tapped on the phone + val phoneIntent = Intent(this, MyPhoneActivity::class.java) + val phonePendingIntent = PendingIntent.getActivity( + this, 0, phoneIntent, PendingIntent.FLAG_IMMUTABLE + ) + + // Intent to fire when tapped on the glasses display + val glassesIntent = Intent(this, MyGlassesActivity::class.java) + val glassesPendingIntent = PendingIntent.getActivity( + this, 1, glassesIntent, PendingIntent.FLAG_IMMUTABLE + ) + + // Create the base notification + val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply { + setSmallIcon(R.drawable.ic_notification) + setContentTitle("Navigation in Progress") + setContentText("Tap to see details") + // Default intent for phone + setContentIntent(phonePendingIntent) + + // Create and apply the glasses extender + val projectedExtender = ProjectedExtender().setContentIntent(glassesPendingIntent) + + extend(projectedExtender) + } + + // Issue the notification + notificationManager.notify(NOTIFICATION_ID, builder.build()) + +### Java + + // Intent to fire when tapped on the phone + Intent phoneIntent = new Intent(this, MyPhoneActivity.class); + PendingIntent phonePendingIntent = + PendingIntent.getActivity(this, 0, phoneIntent, PendingIntent.FLAG_IMMUTABLE); + + // Intent to fire when tapped on the glasses display + Intent glassesIntent = new Intent(this, MyGlassesActivity.class); + PendingIntent glassesPendingIntent = + PendingIntent.getActivity(this, 1, glassesIntent, PendingIntent.FLAG_IMMUTABLE); + + // Create the base notification + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle("New Update") + .setContentText("Something important happened.") + // Default intent for phone + .setContentIntent(phonePendingIntent); + + // Create and apply the Glasses extender + ProjectedExtender projectedExtender = new ProjectedExtender() + // glasses-specific intent + .setContentIntent(glassesPendingIntent); + builder.extend(projectedExtender); + + // Issue the notification + notificationManager.notify(NOTIFICATION_ID, builder.build()); \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md new file mode 100644 index 0000000..b19399d --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md @@ -0,0 +1,65 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +In Jetpack Compose Glimmer, the [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.xr.glimmer.ButtonSize,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) component is an interactive +component that's optimized for AI glasses input, offering clear visual feedback +for their unfocused, focused, and pressed states to guide user actions. +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons.png) **Figure 1.** An example of some different styles of buttons in Jetpack Compose Glimmer. + +## Example: Button variations + + @Composable + fun GlimmerButtonExample() { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + // Basic Button + Button(onClick = { /* Do something */ }) { + Text("Test Button 1") + } + + // Button with a leading icon + Button( + onClick = { /* Do something */ }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_favorite), + contentDescription = "Favorite icon" + ) + } + ) { + Text("Test Button 2") + } + + // Button with leading and trailing icons + Button( + onClick = { /* Do something */ }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_favorite), + contentDescription = "Favorite icon" + ) + }, + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_favorite), + contentDescription = "Favorite icon" + ) + } + ) { + Text("Test Button 3") + } + } + } + +### Key points about the code + +- The button icons source local XML vector drawables (`R.drawable.ic_favorite`) using [`painterResource`](https://developer.android.com/reference/kotlin/androidx/compose/ui/res/painterResource.composable#painterResource(kotlin.Int)), replacing the `Icons.Default` library dependency for optimized asset loading. +- The `leadingIcon` and `trailingIcon` parameters are utilized to inject icon Composables into the button layout, demonstrating Jetpack Compose Glimmer's support for flexible icon positioning. +- The buttons use the default sizing configuration, which automatically manages internal padding and text scaling to align with standard Jetpack Compose Glimmer design specifications without explicit size modifiers. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md new file mode 100644 index 0000000..90e40e9 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md @@ -0,0 +1,81 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +In Jetpack Compose Glimmer, the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) component is designed to group and +display related information in a single unit. Cards are highly adaptable, +supporting combinations of main content, optional titles and subtitles, and +leading or trailing icons. Cards fill the maximum full width of the AI glasses +display by default, are focusable, and can also be made clickable. +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_cards.png) **Figure 1.** An example of some different styles of cards in Jetpack Compose Glimmer. + +## Card Anatomy and Slots + +A Jetpack Compose Glimmer Card is built from several specialized elements, +allowing you to customize its content and layout. + +- **Header**: The top section of the card, designed to hold an image. + +- **Title and Subtitle**: These text fields provide the main and secondary + labels for the card. + +- **Leading Icon**: An optional icon that appears at the beginning of the + card's content area. + +- **Trailing Icon**: An optional icon that appears at the end of the card's + content area. + +- **Action**: A slot for a primary interactive element, such as a Button. This + allows users to perform an action directly from the card. The slot is + available on a separate overload of the Card composable. + +- **Main Content**: The core body of the card, where you place the primary + Text or other content. + +## Basic example: Create a basic card + +You can create a very basic card with very little code: + + Card { Text("This is a card") } + +## Detailed example: Display a complex card + +The following code shows how to use multiple slots together to build a card. It +also shows how to pair a [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) with a [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)). + + @Composable + fun SampleGlimmerCard() { + val myHeaderImage = painterResource(id = R.drawable.header_image) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TitleChip { Text("Title Chip") } + Spacer(Modifier.height(TitleChipDefaults.AssociatedContentSpacing)) + Card( + action = { + Button(onClick = {}) { + Text("Action Button") + } + }, + header = { + Image( + painter = myHeaderImage, + contentDescription = "Header image for the card", + contentScale = ContentScale.FillWidth + ) + }, + title = { Text("Card Title") }, + subtitle = { Text("Card Subtitle") }, + leadingIcon = { FavoriteIcon, "Localized description" }, + trailingIcon = { FavoriteIcon, "Localized description" } + ) { + Text("A Jetpack Compose Glimmer Card using all available slots") + } + } + } + +### Key points about the code + +- Shows how to utilize various card elements, such as [`header`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`title`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`subtitle`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`leadingIcon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), and [`action`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), to customize the card's content and structure. +- Shows an example of how to place a [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) and use a [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)). \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md new file mode 100644 index 0000000..90a80b4 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md @@ -0,0 +1,98 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +All Jetpack Compose Glimmer components are designed to work with standard input +methods similar to phones, such as a tap or swipe on the AI glasses' touchpad, +while also being receptive to input commands that are specific to AI glasses +hardware, such as [camera and display buttons](https://developer.android.com/design/ui/ai-glasses/guides/interaction/inputs). Jetpack Compose Glimmer +components automatically handle the necessary input events. For custom +components, you can utilize existing Compose APIs like +[`Modifier.draggable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)) or [`Modifier.scrollable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)) to implement specific +interaction behaviors. + +On AI glasses with a display, pointer input can affect focus: + +- **Tap**: Direct interaction for activating element. Focus moves to an element when a user interacts with it. +- **Swipe**: Used for navigation and scrolling. Unhandled swipe gestures automatically translate into focus movements, enabling seamless UI navigation without direct pointer input. + +
+ +> [!WARNING] +> **(Temporary requirement) Enable required input flag** : +> +>
+> +> Using a feature of Jetpack Compose, the system can automatically set the initial +> focus to the very-first focusable element when the screen loads, which is often +> the top-left item on the screen. This feature is still in development and isn't +> enabled by default. To activate this feature, set the +> `isInitialFocusOnFocusableAvailable` flag to `true` in your activity's +> [`onCreate()`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method. +> +> class GlassesActivityExample : ComponentActivity() { +> override fun onCreate(savedInstanceState: Bundle?) { +> @OptIn(ExperimentalComposeUiApi::class) +> ComposeUiFlags.isInitialFocusOnFocusableAvailable = true +> super.onCreate(savedInstanceState) +> } +> } +> +>
+> +
+ +## Navigation behavior and order + +Focus movement and order change as a user navigates your app. + +### Focus movement + +On a scrollable container, focus moves continuously with a swipe on the +touchpad. For discrete elements like a row of buttons, each swipe moves the +focus one element at a time. + +### Focus order + +Just like in Jetpack Compose, Jetpack Compose Glimmer uses one-dimensional focus +search. To learn more about the order of focus traversal, see [Change focus +traversal order](https://developer.android.com/develop/ui/compose/touch-input/focus/change-focus-traversal-order#override-one-dimensional). + +To change the initially-focused item, you can add a top-level +[`Modifier.focusGroup()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).focusGroup()) and specify a custom `onEnter` +[`focusProperty`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).focusProperties(kotlin.Function1)): + + Modifier.focusProperties { + onEnter = { + initialFocus.requestFocus() + cancelFocusChange() + } + } + .focusGroup() + +### Scrolling containers + +For an optimal user experience, scrolling containers like lists should be the +only major component on a screen. Avoid placing a scrollable list directly above +or below other interactive elements, such as buttons, to prevent navigation +confusion and promote smooth, predictable focus movement. + +## Default focus states + +Jetpack Compose Glimmer implements default focus states across its interactable +components, including surfaces, cards, and list items, promoting consistent and +clear visual feedback during user interaction. +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_ixd_inputs_focus.png) **Figure 1.** Three focus states in Jetpack Compose Glimmer, which are differentiated using outline-based visual feedback. + +- **Default** : The button's background color is derived from + [`GlimmerTheme.colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()), its main content calculates the content + color of that surface, and icons are [`GlimmerTheme.colors.primary`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#primary()). + +- **Focused**: The border width is increased to communicate focus. + +- **Focused + Pressed** : The background is set to + `GlimmerTheme.colors.surface` at full opacity to communicate its selected + state. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md new file mode 100644 index 0000000..94dcb98 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md @@ -0,0 +1,40 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +In Jetpack Compose Glimmer, the [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier)) component is specifically designed +for rendering single-color icons. `Icon` can accept [`ImageVector`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector), +[`ImageBitmap`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ImageBitmap), or [`Painter`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter) as its source. `Icon`, similar to +[`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)), can intelligently apply a tint based on the surrounding UI theme. +While it defaults to a size provided by the [`LocalIconSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#LocalIconSize()), you can also +set custom icon sizes. + +## Example: Create a box with a large star icon + + @Composable + fun GlimmerIconSample() { + GlimmerTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = painterResource(id = R.drawable.ic_star), + contentDescription = "A star icon from Google Symbols", + modifier = Modifier.size(GlimmerTheme.iconSizes.large), + tint = GlimmerTheme.colors.primary + ) + } + } + } + } + +### Key points about the code + +- The icon's source loads a local XML vector drawable (`R.drawable.ic_star`) using [`painterResource`](https://developer.android.com/reference/kotlin/androidx/compose/ui/res/painterResource.composable#painterResource(kotlin.Int)), demonstrating the recommended approach for integrating icons into a Jetpack Compose Glimmer UI without external library overhead. +- The icon's size is customized by setting [`GlimmerTheme.iconSizes.large`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes#large()) with a modifier, demonstrating how to override Jetpack Compose Glimmer's predefined sizing. +- The icon's tint color is customized by setting [`GlimmerTheme.colors.primary`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#primary()) using the tint parameter, applying single-color icon tinting for visual consistency. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md new file mode 100644 index 0000000..3b2b89f --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md @@ -0,0 +1,44 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +In Jetpack Compose Glimmer, the [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.text.TextStyle)) component builds upon the basic text +and lets you set various text properties like color, font size, font style, font +weight, font family, letter spacing, and text alignment. The Jetpack Compose +Glimmer `Text` component is unique in that it intelligently manages color +matching. For example, if no color override is specified, the text defaults to +the content color provided by the nearest surface in the UI hierarchy. + +## Example: Create a text heading in a box + + @Composable + fun GlimmerStyleSample() { + GlimmerTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "This is a sample heading", + color = GlimmerTheme.colors.secondary + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle Click */ }) { + Text(text = "Sample Button") + } + } + } + } + } + +### Key points about the code + +- The [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.xr.glimmer.ButtonSize,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) composable is automatically interactable, has a + [`Colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()) background, and the text is automatically set to: + + - style = [`GlimmerTheme.typography.bodyMedium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodyMedium()) + - color = [`GlimmerTheme.Colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()) \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md new file mode 100644 index 0000000..7a83fe3 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md @@ -0,0 +1,52 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +In Jetpack Compose Glimmer, the [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) component is designed to +provide brief, non-interactive label for associated content, such as a Card. Use +title chips to display concise information like a short title, a name, or a +status. Since title chips are not focusable or interactive, they serve a purely +informational role within the a Jetpack Compose Glimmer UI. For example, you +might provide a title chip labeled "Ingredients" next to a scrollable list of +ingredients. +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_titlechip_anatomy.png) **Figure 1.** An example of some different styles of title chips in Jetpack Compose Glimmer. + +Default title chip and Sticky title chip shown. Sticky title chips are displayed +with an outline. + +1. Title chips label +2. Optional leading icon or entity + +## Basic example: Display a short title chip + +You can create a short title chip with very little code: + + TitleChip { Text("Messages") } + +## Detailed example: Display a title chip with a card + +To use a title chip with another component, place the title chip +[`TitleChipDefaults.AssociatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#AssociatedContentSpacing()) above the other component in +the composable. The following code shows how to use a title chip with a card: + + @Composable + fun TitleChipExample() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TitleChip { Text("Title Chip") } + Spacer(Modifier.height(TitleChipDefaults.AssociatedContentSpacing)) + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Card Content") + } + } + } + +### Key points about the code + +- The [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)) has a fixed height to provide the correct vertical spacing, defined by [`TitleChipDefaults.AssociatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#AssociatedContentSpacing()), between the two components. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md new file mode 100644 index 0000000..450dd4b --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md @@ -0,0 +1,177 @@ +
+ + +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) + +
+ +Just like on a phone, accessing sensitive hardware like the camera and +microphone on AI glasses requires explicit user consent. These are considered +**glasses-specific permissions**, and your app must request them at runtime, +even if it already has the corresponding permissions on the phone. + +## Declare the permissions in your app's manifest + +Before requesting permissions, you must [declare them in your app's manifest](https://developer.android.com/training/permissions/declaring) +file using the [``](https://developer.android.com/guide/topics/manifest/uses-permission-element) element. This declaration remains the +same whether the permission is for a phone or an AI-glasses-specific feature, +but you must still explicitly request it for glasses-specific hardware or +functionality. + + + + + + ... + + + +## Register the permissions launcher + +To request permissions for AI glasses, first you use the +[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the +[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method to register the permissions +launcher. + + +```kotlin +// Register the permissions launcher using the ProjectedPermissionsResultContract. +private val requestPermissionLauncher: ActivityResultLauncher> = + registerForActivityResult(ProjectedPermissionsResultContract()) { results -> + if (results[Manifest.permission.CAMERA] == true) { + isPermissionDenied = false + initializeGlassesFeatures() + } else { + // Handle permission denial. + isPermissionDenied = true + } + } +``` + +
+ +### Key points about the code + +- The code creates an [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method. The callback receives a map of permission names to their granted status. +- You need to specify which permissions your app requires, such as [`Manifest.permission.CAMERA`](https://developer.android.com/reference/kotlin/android/Manifest.permission#camera) or [`Manifest.permission.RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio). + +## Create the request function + +Next, you'll create a function that uses your app's permissions launcher to +request the permissions from the user at runtime. + + +```kotlin +private fun requestHardwarePermissions() { + val params = ProjectedPermissionsRequestParams( + permissions = listOf(Manifest.permission.CAMERA), + rationale = "Camera access is required to overlay digital content on your physical environment." + ) + requestPermissionLauncher.launch(listOf(params)) +} +``` + +
+ +### Key points about the code + +- The `requestHardwarePermissions` function builds a [`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) object. This object bundles the list of permissions your app needs and the user-facing rationale. Make the rationale clear and concise to explain why your app needs these permissions. +- Calling `launch` on the launcher triggers the [permission request user + flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow). +- Your app should handle both granted and denied results gracefully in the launcher's callback. + +## Create the permissions check function + +Next, you'll create a function that can check whether the user has granted +permissions to your app. + + +```kotlin +private fun hasCameraPermission(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED +} +``` + +
+ +## Add the permission request logic + +And lastly, create the logic that uses these functions to check for and request +the permissions at runtime. + + +```kotlin +if (hasCameraPermission()) { + initializeGlassesFeatures() +} else { + requestHardwarePermissions() +} +``` + +
+ +### Key points about the code + +- If the user has already granted your app the required permissions, the `initializeGlassesFeatures` function is called to initialize your app's experience. This function is defined as [part of your app's activity for AI + glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity#create-activity). + +## Understand the permission request user flow + +When you launch a permission request using the +[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method, the system initiates a +coordinated user flow across both the AI glasses and the phone. + +
+ +> [!IMPORTANT] +> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on AI glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow. +> +>
+> +> If your app already has an `Activity` displayed on the phone, you should use +> [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1), where +> the `deviceId` comes from calling the [`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) method on the +> [`Context`](https://developer.android.com/reference/kotlin/android/content/Context) returned by calling +> [`ProjectedContext.createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)). +> +>
+> +
+ +During the permissions user flow, here is what your app and the user can expect: + +1. **On the AI glasses** : An activity appears on the **projected device + (glasses)**, instructing the user to look at their phone to continue. + +
+ + > [!WARNING] + > **Preview:** Currently, the instructions and rationale provided by the system are not audible to the user. To provide an audible rationale to the user, we recommend using [Text to Speech + > (TTS)](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts). For example: + > + >
+ > + > tts?.speak("Please review the permission request on your host device", + > TextToSpeech.QUEUE_ADD, + > null, + > "permission_request") + > + >
+ > +
+ +2. **On the phone** : Concurrently, an activity launches on the **host device + (phone)**. This screen displays the rationale string you provided and gives + the user the option to proceed or cancel. + +3. **On the phone** : If the user accepts the rationale, a modified Android + system permission dialog appears on the phone telling the user that they are + granting the permission **for the AI glasses device** (not the phone), and + the user can formally grant or deny the permission. + +4. **Receiving the result** : After the user makes their final choice, the + activities on both the phone and AI glasses are dismissed. Your + [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map containing + the granted status for each requested permission. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md new file mode 100644 index 0000000..902f60f --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md @@ -0,0 +1,176 @@ +When creating a Glimmer Button component, refer to the following implementation +samples in `ButtonSamples.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.glimmer.Button +import androidx.xr.glimmer.ButtonSize +import androidx.xr.glimmer.GlimmerTheme +import androidx.xr.glimmer.Icon +import androidx.xr.glimmer.Text +import androidx.xr.glimmer.list.VerticalList + +@Composable +fun ButtonSampleUsage() { + VerticalList( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), + ) { + item { ButtonSample() } + item { ButtonWithLeadingIconSample() } + item { ButtonWithTrailingIconSample() } + item { ButtonWithLeadingAndTrailingIconSample() } + item { LargeButtonSample() } + item { LargeButtonWithLeadingIconSample() } + item { LargeButtonWithTrailingIconSample() } + item { LargeButtonWithLeadingAndTrailingIconSample() } + } +} + +@Sampled +@Composable +fun ButtonSample() { + Button(onClick = {}) { Text("Send") } +} + +@Sampled +@Composable +fun ButtonWithLeadingIconSample() { + Button(onClick = {}, leadingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") + } +} + +@Composable +private fun ButtonWithTrailingIconSample() { + Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") + } +} + +@Composable +private fun ButtonWithLeadingAndTrailingIconSample() { + Button( + onClick = {}, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Sampled +@Composable +fun LargeButtonSample() { + Button(onClick = {}, buttonSize = ButtonSize.Large) { Text("Send") } +} + +@Composable +private fun LargeButtonWithLeadingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Composable +private fun LargeButtonWithTrailingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Composable +private fun LargeButtonWithLeadingAndTrailingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Preview +@Composable +private fun ButtonPreview() { + GlimmerTheme { ButtonSample() } +} + +@Preview +@Composable +private fun ButtonWithLeadingIconPreview() { + GlimmerTheme { ButtonWithLeadingIconSample() } +} + +@Preview +@Composable +private fun ButtonWithTrailingIconPreview() { + GlimmerTheme { ButtonWithTrailingIconSample() } +} + +@Preview +@Composable +private fun ButtonWithLeadingAndTrailingIconPreview() { + GlimmerTheme { ButtonWithLeadingAndTrailingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonPreview() { + GlimmerTheme { LargeButtonSample() } +} + +@Preview +@Composable +private fun LargeButtonWithLeadingIconPreview() { + GlimmerTheme { LargeButtonWithLeadingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonWithTrailingIconPreview() { + GlimmerTheme { LargeButtonWithTrailingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonWithLeadingAndTrailingIconPreview() { + GlimmerTheme { LargeButtonWithLeadingAndTrailingIconSample() } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md new file mode 100644 index 0000000..9c3caef --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md @@ -0,0 +1,205 @@ +When creating a Glimmer Button component, refer to the following source code in +`Button.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Button is a component used for exposing actions to a user. + * + * @sample androidx.xr.glimmer.samples.ButtonSample + * + * Buttons can use icons to provide more context about the action: + * + * @sample androidx.xr.glimmer.samples.ButtonWithLeadingIconSample + * + * There are multiple button size variants - providing a different [ButtonSize] will affect default + * values used inside this button, such as the minimum height. Note that you can still provide a + * size modifier such as [androidx.compose.foundation.layout.size] to change the layout size of this + * button, [buttonSize] affects default values and values internal to the button. + * + * @sample androidx.xr.glimmer.samples.LargeButtonSample + * @param onClick called when this button is clicked + * @param modifier the [Modifier] to be applied to this button + * @param enabled controls the enabled state of this button. When `false`, this button will not + * respond to user input + * @param buttonSize the size variant of this button, represented as a [ButtonSize]. Changing + * [buttonSize] will affect some default values used by this button - but the final resulting size + * of the button will still be calculated based on the content of the button, and any provided + * size modifiers such as [androidx.compose.foundation.layout.size]. For example, setting a 100.dp + * size using a size modifier will result in the same layout size regardless of [buttonSize], but + * the provided [buttonSize] will affect other properties such as padding values and the size of + * icons. + * @param leadingIcon optional leading icon to be placed before the [content]. This is typically an + * [Icon]. + * @param trailingIcon optional trailing icon to be placed after the [content]. This is typically an + * [Icon]. + * @param shape the [Shape] used to clip this button, and also used to draw the background and + * border + * @param color background color of this button + * @param contentColor content color used by components inside [content], [leadingIcon], and + * [trailingIcon]. + * @param border the border to draw around this button + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this button. You can use this to change the button's appearance or + * preview the button in different states. Note that if `null` is provided, interactions will + * still happen internally. + * @param content the main content, typically [Text], to display inside this button + */ +@Composable +public fun Button( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + buttonSize: ButtonSize = ButtonSize.Medium, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.large, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = ButtonDefaults.contentPadding(buttonSize), + interactionSource: MutableInteractionSource? = null, + content: @Composable RowScope.() -> Unit, +) { + val iconSize = ButtonDefaults.iconSize + val iconSpacing = ButtonDefaults.iconSpacing + val minHeight = ButtonDefaults.minimumHeight(buttonSize) + + val depth = + SurfaceDepthEffect( + depthEffect = null, + focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1, + ) + + CompositionLocalProvider(LocalTextStyle provides GlimmerTheme.typography.bodySmall) { + Row( + modifier + .semantics { role = Role.Button } + .surface( + enabled = enabled, + shape = shape, + color = color, + contentColor = contentColor, + depthEffect = depth, + border = border, + interactionSource = interactionSource, + onClick = onClick, + ) + .defaultMinSize(minHeight = minHeight) + .padding(contentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (leadingIcon != null) { + Box(Modifier.padding(end = iconSpacing)) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) + } + } + content() + if (trailingIcon != null) { + Box(Modifier.padding(start = iconSpacing)) { + CompositionLocalProvider( + LocalIconSize provides iconSize, + content = trailingIcon, + ) + } + } + } + } +} + +/** + * ButtonSize represents the different size variants of a [Button]. ButtonSize will affect default + * values used inside a [Button], such as the minimum height and the size of icons. + */ +@Immutable +@JvmInline +public value class ButtonSize internal constructor(private val value: Int) { + public companion object { + /** ButtonSize representing a medium [Button]. This is the default size. */ + public val Medium: ButtonSize = ButtonSize(1) + /** ButtonSize representing a large [Button]. */ + public val Large: ButtonSize = ButtonSize(2) + } +} + +/** Default values used for [Button]. */ +public object ButtonDefaults { + /** Default content padding used for a [Button] with the specified [buttonSize]. */ + @Composable + public fun contentPadding(buttonSize: ButtonSize): PaddingValues { + val componentSpacingValues = GlimmerTheme.componentSpacingValues + return if (buttonSize == ButtonSize.Medium) { + PaddingValues( + horizontal = componentSpacingValues.large, + vertical = componentSpacingValues.small, + ) + } else { + PaddingValues(componentSpacingValues.large) + } + } + + /** Default minimum height for [Button] and [ToggleButton] with the specified [buttonSize]. */ + internal fun minimumHeight(buttonSize: ButtonSize): Dp { + return when (buttonSize) { + ButtonSize.Medium -> 48.dp + ButtonSize.Large -> 72.dp + else -> throw IllegalArgumentException("Unknown size $buttonSize.") + } + } + + /** Default icon size for buttons with non-icon content: [Button], [ToggleButton]. */ + @get:Composable + internal val iconSize: Dp + get() = GlimmerTheme.iconSizes.small + + /** Default spacing between icon and content for [Button] and [ToggleButton]. */ + @get:Composable + internal val iconSpacing: Dp + get() = GlimmerTheme.componentSpacingValues.extraSmall +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md new file mode 100644 index 0000000..ce9da11 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md @@ -0,0 +1,295 @@ +When creating a Glimmer Card component, refer to the following implementation +samples in `CardSamples.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.glimmer.Button +import androidx.xr.glimmer.Card +import androidx.xr.glimmer.GlimmerTheme +import androidx.xr.glimmer.Icon +import androidx.xr.glimmer.Text +import androidx.xr.glimmer.list.VerticalList + +@Composable +fun CardSampleUsage() { + VerticalList { + item { CardSample() } + item { CardWithTrailingIconSample() } + item { CardWithTitleAndSubtitleAndLeadingIconSample() } + item { CardWithTitleAndHeaderSample() } + item { CardWithTitleAndActionSample() } + item { CardWithTitleAndLeadingIconAndHeader() } + item { CardWithTitleAndLeadingIconAndHeaderAndAction() } + item { CardWithLongText() } + item { CardWithTitleAndSubtitleAndLeadingIconLongText() } + item { CardWithTitleAndSubtitleAndLeadingIconAndTrailingIconLongText() } + } +} + +@Sampled +@Composable +fun CardSample() { + Card { Text("This is a card") } +} + +@Sampled +@Composable +fun CardWithTrailingIconSample() { + Card(trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("This is a card with a trailing icon") + } +} + +@Sampled +@Composable +fun CardWithTitleAndSubtitleAndLeadingIconSample() { + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("This is a card with a title, subtitle, and leading icon") + } +} + +@Sampled +@Composable +fun CardWithTitleAndHeaderSample() { + Card( + title = { Text("Title") }, + header = { + Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth) + }, + ) { + Text("This is a card with a title and header image") + } +} + +@Sampled +@Composable +fun CardWithTitleAndActionSample() { + Card(action = { Button(onClick = {}) { Text("Send") } }, title = { Text("Title") }) { + Text("This is a card with a title and action") + } +} + +@Sampled +@Composable +fun ClickableCardSample() { + Card(onClick = {}) { Text("This is a card") } +} + +@Sampled +@Composable +fun ClickableCardWithTrailingIconSample() { + Card(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("This is a card with a trailing icon") + } +} + +@Sampled +@Composable +fun ClickableCardWithTitleAndSubtitleAndLeadingIconSample() { + Card( + onClick = {}, + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("This is a card with a title, subtitle, and leading icon") + } +} + +@Sampled +@Composable +fun ClickableCardWithTitleAndHeaderSample() { + Card( + onClick = {}, + title = { Text("Title") }, + header = { + Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth) + }, + ) { + Text("This is a card with a title and header image") + } +} + +@Composable +fun CardWithTitleAndLeadingIconAndHeader() { + Card( + title = { Text("Title") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + header = { + Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth) + }, + ) { + Text("This is a card with a title, leading icon, and header image") + } +} + +@Composable +fun CardWithTitleAndLeadingIconAndHeaderAndAction() { + Card( + action = { + Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") + } + }, + title = { Text("Title") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + header = { + Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth) + }, + ) { + Text("This is a card with a title, leading icon, header image, and action") + } +} + +@Composable +fun CardWithLongText() { + Card { + Text( + "This is a card with a lot of text that will wrap to multiple lines. The maximum recommend number of lines of text for a card is 10.", + maxLines = 10, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +fun CardWithTitleAndSubtitleAndLeadingIconLongText() { + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text( + "This is a card with a lot of text that will wrap to multiple lines. The maximum recommend number of lines of text for a card is 10." + ) + } +} + +@Composable +fun CardWithTitleAndSubtitleAndLeadingIconAndTrailingIconLongText() { + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text( + "This is a card with a lot of text that will wrap to multiple lines. The maximum recommend number of lines of text for a card is 10." + ) + } +} + +@Preview +@Composable +private fun CardPreview() { + GlimmerTheme { CardSample() } +} + +@Preview +@Composable +private fun CardWithTrailingIconPreview() { + GlimmerTheme { CardWithTrailingIconSample() } +} + +@Preview +@Composable +private fun CardWithTitleAndSubtitleAndLeadingIconPreview() { + GlimmerTheme { CardWithTitleAndSubtitleAndLeadingIconSample() } +} + +@Preview +@Composable +private fun CardWithTitleAndHeaderPreview() { + GlimmerTheme { CardWithTitleAndHeaderSample() } +} + +@Preview +@Composable +private fun CardWithTitleAndActionPreview() { + GlimmerTheme { CardWithTitleAndActionSample() } +} + +@Preview +@Composable +private fun CardWithTitleAndLeadingIconAndHeaderPreview() { + GlimmerTheme { CardWithTitleAndLeadingIconAndHeader() } +} + +@Preview +@Composable +private fun CardWithTitleAndLeadingIconAndHeaderAndActionPreview() { + GlimmerTheme { CardWithTitleAndLeadingIconAndHeaderAndAction() } +} + +@Preview +@Composable +private fun CardWithLongTextPreview() { + GlimmerTheme { CardWithLongText() } +} + +@Preview +@Composable +private fun CardWithTitleAndSubtitleAndLeadingIconLongTextPreview() { + GlimmerTheme { CardWithTitleAndSubtitleAndLeadingIconLongText() } +} + +@Preview +@Composable +private fun CardWithTitleAndSubtitleAndLeadingIconAndTrailingIconLongTextPreview() { + GlimmerTheme { CardWithTitleAndSubtitleAndLeadingIconAndTrailingIconLongText() } +} + +fun placeholderImagePainter(intrinsicSize: Size): Painter = + BrushPainter( + Brush.linearGradient( + 0.0f to Color(0xFF3C8CDE), + 0.4f to Color(0xFFED73A8), + 0.6f to Color(0xFFED73A8), + 1.0f to Color(0xFFE763F9), + start = Offset.Zero, + end = Offset(intrinsicSize.width, intrinsicSize.height), + ) + ) + +/** + * Placeholder image with a large intrinsic size, to simulate a real life use case of loading a + * bitmap + */ +private val MyHeaderImage = placeholderImagePainter(Size(1000f, 1000f)) +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md new file mode 100644 index 0000000..5d87c22 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md @@ -0,0 +1,588 @@ +When creating a Glimmer Card component, refer to the following source code in +`Card.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap +import kotlin.math.max + +/** + * Card is a component used to group related information into a single digestible unit. A card can + * adapt to display a wide range of content, from simple text blurbs to more complex summaries with + * multiple elements. A card contains text [content], and may also have any combination of [title], + * [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the + * [subtitle], which is placed on top of the [content]. A card fills the maximum width available by + * default. + * + * This Card is focusable - see the other [Card] overload for a clickable Card. + * + * Cards can also be combined with a [TitleChip]. See the documentation for [TitleChip] for more + * information / sample code. + * + * A simple Card with just text: + * + * @sample androidx.xr.glimmer.samples.CardSample + * + * A Card with a trailing icon: + * + * @sample androidx.xr.glimmer.samples.CardWithTrailingIconSample + * + * A Card with a title, subtitle, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.CardWithTitleAndSubtitleAndLeadingIconSample + * + * A card with a title and a header image: + * + * @sample androidx.xr.glimmer.samples.CardWithTitleAndHeaderSample + * @param modifier the [Modifier] to be applied to this card + * @param title optional title to be placed above [subtitle] and [content], below [header] + * @param subtitle optional subtitle to be placed above [content], below [title] + * @param header optional header image to be placed at the top of the card. This image should + * typically fill the max width available, for example using + * [androidx.compose.ui.layout.ContentScale.FillWidth]. Headers are constrained to a maximum + * aspect ratio (1.6) to avoid taking up too much vertical space, so using a modifier such as + * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum + * aspect ratio. + * @param leadingIcon optional leading icon to be placed before [content]. This is typically an + * [Icon]. + * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an + * [Icon]. + * @param shape the [Shape] used to clip this card, and also used to draw the background and border + * @param color background color of this card + * @param contentColor content color used by components inside [content], [title], [subtitle], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this card + * @param contentPadding the spacing values to apply internally between the container and the + * content. Note that there is additional padding applied around the content / text / icons inside + * a card, this only affects the outermost content padding. + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this card. You can use this to change the card's appearance or + * preview the card in different states. Note that if `null` is provided, interactions will still + * happen internally. + * @param content the main content / body text to display inside this card. This is recommended to + * be limited to 10 lines of text. + */ +@Composable +public fun Card( + modifier: Modifier = Modifier, + title: @Composable (() -> Unit)? = null, + subtitle: @Composable (() -> Unit)? = null, + header: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = CardDefaults.shape, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = CardDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + CardImpl( + modifier = modifier, + onClick = null, + focusable = true, + title = title, + subtitle = subtitle, + header = header, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +/** + * Card is a component used to group related information into a single digestible unit. A card can + * adapt to display a wide range of content, from simple text blurbs to more complex summaries with + * multiple elements. A card contains text [content], and may also have any combination of [title], + * [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the + * [subtitle], which is placed on top of the [content]. A card fills the maximum width available by + * default. + * + * This Card is focusable and clickable - see the other [Card] overload for a Card that is only + * focusable. + * + * Cards can also be combined with a [TitleChip]. See the documentation for [TitleChip] for more + * information / sample code. + * + * A simple clickable Card with just text: + * + * @sample androidx.xr.glimmer.samples.ClickableCardSample + * + * A clickable Card with a trailing icon: + * + * @sample androidx.xr.glimmer.samples.ClickableCardWithTrailingIconSample + * + * A clickable Card with a title, subtitle, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.ClickableCardWithTitleAndSubtitleAndLeadingIconSample + * + * A clickable Card with a title and a header image: + * + * @sample androidx.xr.glimmer.samples.ClickableCardWithTitleAndHeaderSample + * @param onClick called when this card item is clicked + * @param modifier the [Modifier] to be applied to this card + * @param title optional title to be placed above [subtitle] and [content], below [header] + * @param subtitle optional subtitle to be placed above [content], below [title] + * @param header optional header image to be placed at the top of the card. This image should + * typically fill the max width available, for example using + * [androidx.compose.ui.layout.ContentScale.FillWidth]. Headers are constrained to a maximum + * aspect ratio (1.6) to avoid taking up too much vertical space, so using a modifier such as + * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum + * aspect ratio. + * @param leadingIcon optional leading icon to be placed before [content]. This is typically an + * [Icon]. + * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an + * [Icon]. + * @param shape the [Shape] used to clip this card, and also used to draw the background and border + * @param color background color of this card + * @param contentColor content color used by components inside [content], [title], [subtitle], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this card + * @param contentPadding the spacing values to apply internally between the container and the + * content. Note that there is additional padding applied around the content / text / icons inside + * a card, this only affects the outermost content padding. + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this card. You can use this to change the card's appearance or + * preview the card in different states. Note that if `null` is provided, interactions will still + * happen internally. + * @param content the main content / body text to display inside this card. This is recommended to + * be limited to 10 lines of text. + */ +@Composable +public fun Card( + onClick: () -> Unit, + modifier: Modifier = Modifier, + title: @Composable (() -> Unit)? = null, + subtitle: @Composable (() -> Unit)? = null, + header: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = CardDefaults.shape, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = CardDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + CardImpl( + modifier = modifier, + onClick = onClick, + focusable = true, + title = title, + subtitle = subtitle, + header = header, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +/** + * Card is a component used to group related information into a single digestible unit. A card can + * adapt to display a wide range of content, from simple text blurbs to more complex summaries with + * multiple elements. A card contains text [content], and may also have any combination of [title], + * [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the + * [subtitle], which is placed on top of the [content]. A card fills the maximum width available by + * default. + * + * This Card contains an [action] that is placed on the center of the bottom edge of the card. The + * action should be a [Button], and represents the action that will be performed when this card is + * interacted with. The main card itself is not focusable - the [action] takes the focus instead. + * + * For more documentation and samples of the other card parameters, see the other card overload + * without an action. + * + * @sample androidx.xr.glimmer.samples.CardWithTitleAndActionSample + * @param action the action for this card. This should be a [Button], and represents the action + * performed when a user interacts with this card. The action is placed overlapping the bottom + * edge of the card. + * @param modifier the [Modifier] to be applied to the outer layout containing the card and action + * @param title optional title to be placed above [subtitle] and [content], below [header] + * @param subtitle optional subtitle to be placed above [content], below [title] + * @param header optional header image to be placed at the top of the card. This image should + * typically fill the max width available, for example using + * [androidx.compose.ui.layout.ContentScale.FillWidth]. Headers are constrained to a maximum + * aspect ratio (1.6) to avoid taking up too much vertical space, so using a modifier such as + * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum + * aspect ratio. + * @param leadingIcon optional leading icon to be placed before [content]. This is typically an + * [Icon]. + * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an + * [Icon]. + * @param shape the [Shape] used to clip this card, and also used to draw the background and border + * @param color background color of this card + * @param contentColor content color used by components inside [content], [title], [subtitle], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this card + * @param contentPadding the spacing values to apply internally between the container and the + * content. Note that there is additional padding applied around the content / text / icons inside + * a card, this only affects the outermost content padding. + * @param content the main content / body text to display inside this card. This is recommended to + * be limited to 10 lines of text. + */ +@Composable +public fun Card( + action: @Composable () -> Unit, + modifier: Modifier = Modifier, + title: @Composable (() -> Unit)? = null, + subtitle: @Composable (() -> Unit)? = null, + header: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = CardDefaults.shape, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = CardDefaults.contentPadding, + content: @Composable () -> Unit, +) { + // b/436852852 - in a list the button won't be focused until it crosses the focus line. + ActionCardLayout(modifier, action) { + CardImpl( + modifier = Modifier, + onClick = null, + focusable = false, + title = title, + subtitle = subtitle, + header = header, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = null, + content = content, + ) + } +} + +@Composable +private fun CardImpl( + modifier: Modifier, + onClick: (() -> Unit)?, + focusable: Boolean, + title: @Composable (() -> Unit)?, + subtitle: @Composable (() -> Unit)?, + header: @Composable (() -> Unit)?, + leadingIcon: @Composable (() -> Unit)?, + trailingIcon: @Composable (() -> Unit)?, + shape: Shape, + color: Color, + contentColor: Color, + border: BorderStroke?, + contentPadding: PaddingValues, + interactionSource: MutableInteractionSource?, + content: @Composable () -> Unit, +) { + val iconSize = GlimmerTheme.iconSizes.large + val typography = GlimmerTheme.typography + val componentSpacingValues = GlimmerTheme.componentSpacingValues + val innerPadding = componentSpacingValues.small + val iconSpacing = componentSpacingValues.medium + val surfaceModifier = + if (onClick != null) { + Modifier.surface( + onClick = onClick, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + interactionSource = interactionSource, + ) + } else { + Modifier.surface( + focusable = focusable, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + interactionSource = interactionSource, + ) + } + Column( + modifier = + modifier + .then(surfaceModifier) + .defaultMinSize(minHeight = MinimumHeight) + .padding(contentPadding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + header?.let { + Box( + Modifier.constrainHeightToAspectRatio(HeaderMaximumAspectRatio).clip(HeaderShape), + contentAlignment = Alignment.Center, + ) { + it() + } + } + Row( + modifier = Modifier.fillMaxWidth().padding(innerPadding), + verticalAlignment = CenterVertically, + ) { + if (leadingIcon != null) { + Box( + modifier = Modifier.align(Alignment.Top).padding(end = iconSpacing), + contentAlignment = Alignment.TopStart, + ) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) + } + } + Column( + Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(TextVerticalSpacing), + ) { + if (title != null) { + CompositionLocalProvider( + LocalTextStyle provides typography.bodyMedium, + content = title, + ) + } + + if (subtitle != null) { + CompositionLocalProvider( + LocalTextStyle provides typography.caption, + content = subtitle, + ) + } + + CompositionLocalProvider( + LocalTextStyle provides typography.bodySmall, + content = content, + ) + } + if (trailingIcon != null) { + Box( + modifier = Modifier.align(Alignment.Top).padding(start = iconSpacing), + contentAlignment = Alignment.TopEnd, + ) { + CompositionLocalProvider( + LocalIconSize provides iconSize, + content = trailingIcon, + ) + } + } + } + } +} + +@Composable +private fun ActionCardLayout( + modifier: Modifier, + action: @Composable () -> Unit, + card: @Composable () -> Unit, +) { + Layout(contents = listOf(action, card), modifier = modifier) { measurables, constraints -> + val actionMeasurables = measurables[0] + val cardMeasurables = measurables[1] + + var actionMaxWidth = 0 + var actionMaxHeight = 0 + var cardMaxWidth = 0 + var cardMaxHeight = 0 + + val actionPlaceables = + actionMeasurables.fastMap { + // Measure the action with relaxed constraints + val placeable = it.measure(constraints.copyMaxDimensions()) + actionMaxWidth = max(actionMaxWidth, placeable.width) + actionMaxHeight = max(actionMaxHeight, placeable.height) + placeable + } + + val actionInset = ActionInset.roundToPx() + + // The card is allowed to take up the total height - the height of the overall layout taken + // up by the action + val heightTakenUpByAction = (actionMaxHeight - actionInset).coerceAtLeast(0) + + // Shrink the height constraints, to account for the action button + val cardMinHeightConstraints = + (constraints.minHeight - heightTakenUpByAction).coerceAtLeast(0) + val cardMaxHeightConstraints = + if (constraints.hasBoundedHeight) { + (constraints.maxHeight - heightTakenUpByAction).coerceAtLeast(0) + } else { + constraints.maxHeight + } + val cardConstraints = + constraints.copy( + minHeight = cardMinHeightConstraints, + maxHeight = cardMaxHeightConstraints, + ) + + val cardPlaceables = + cardMeasurables.fastMap { + val placeable = it.measure(cardConstraints) + cardMaxWidth = max(cardMaxWidth, placeable.width) + cardMaxHeight = max(cardMaxHeight, placeable.height) + placeable + } + + val layoutWidth = maxOf(actionMaxWidth, cardMaxWidth) + val layoutHeight = heightTakenUpByAction + cardMaxHeight + + layout(layoutWidth, layoutHeight) { + cardPlaceables.fastForEach { + // Horizontally center in the overall space + val x = (layoutWidth - it.width) / 2 + it.placeRelative(x, 0) + } + + actionPlaceables.fastForEach { + // Horizontally center in the overall space + val x = (layoutWidth - it.width) / 2 + val y = cardMaxHeight - actionInset + it.placeRelative(x, y) + } + } + } +} + +/** + * Constrains the content's height to a maximum aspect ratio, based on the maximum width. + * + * This modifier is similar to [androidx.compose.foundation.layout.aspectRatio], but it only + * enforces a maximum size, allowing the content to be smaller than the bounds defined by the aspect + * ratio. It also only constrains the height based on the width, it does not constrain the width + * based on the height. + * + * @param widthToHeightRatio the maximum aspect ratio allowed for the height. This is defined as the + * ratio of width / height + */ +private fun Modifier.constrainHeightToAspectRatio(widthToHeightRatio: Float): Modifier { + require(widthToHeightRatio > 0) { "Ratio must be positive" } + return this.layout { measurable, constraints -> + // We only want to constrain height, based on width. If width is unbounded and there is a + // bounded height, we don't want to constrain the width based on height. So do nothing if + // we don't have a constrained width + if (!constraints.hasBoundedWidth) { + val placeable = measurable.measure(constraints) + return@layout layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } + + val height = + (constraints.maxWidth / widthToHeightRatio) + .toInt() + // Handle the case where the width is more than ratio times larger than available + // height + .coerceAtMost(constraints.maxHeight) + + val newConstraints = + constraints.copy( + // Relax minimum height to let the content be smaller than constraints.minHeight if + // the aspect ratio results in a height smaller than min height + minHeight = 0, + maxHeight = height, + ) + + val placeable = measurable.measure(newConstraints) + + // We relaxed the constraints earlier, but we still need to respect the incoming constraints + // ourselves. + val layoutHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight) + + layout(placeable.width, layoutHeight) { + // Center the content within the final layout height if needed + val y = (layoutHeight - placeable.height) / 2 + placeable.placeRelative(0, y) + } + } +} + +/** Default values used for [Card] */ +public object CardDefaults { + /** + * Default content padding used for a [Card] + * + * This affects the outermost content padding applied around header images and the content + * container. Note that there is additional padding applied around the content / text / icons + * inside a card, this only represents the outer padding for the entire content. + */ + public val contentPadding: PaddingValues + @Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.medium) + + /** The default shape of [Card], which determines its corner radius. */ + public val shape: Shape + @Composable get() = GlimmerTheme.shapes.medium +} + +/** Default minimum height for a [Card] */ +private val MinimumHeight = 80.dp + +/** Spacing between title / subtitle / body text */ +private val TextVerticalSpacing = 3.dp + +/** Shape used to clip the header image */ +private val HeaderShape = RoundedCornerShape(24.dp) + +/** + * Width / height aspect ratio for header images, to prevent the images from taking up too much + * vertical space + */ +private const val HeaderMaximumAspectRatio = 1.6f + +/** How far the action button is inset from the underlying card's edge */ +private val ActionInset = 16.dp +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md new file mode 100644 index 0000000..db9d205 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md @@ -0,0 +1,224 @@ +When simulating depth on AI glasses using shadows, refer to the following source +code in `DepthEffect.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.ShadowScope +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.shadow.DropShadowPainter +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.graphics.shadow.lerp +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.requireGraphicsContext +import androidx.compose.ui.platform.InspectorInfo + +/** + * The depth effect establishes a sense of hierarchy by using shadows to occlude content underneath. + * It consists of two shadow layers, [layer1] and [layer2]. [layer2] is drawn on top of [layer1]: + * + * _________________ + * | _________ | + * | | content | | + * | |_________| | + * | ___________ | + * | | layer 2 | | + * | |___________| | + * | _____________ | + * | | layer 1 | | + * | |_____________| | + * |_________________| + * + * [GlimmerTheme.depthEffectLevels] provides theme defined depth effect levels that should be used + * to add depth to surfaces. + * + * Higher level components apply the depth effect automatically when needed. The depth effect can + * also be configured through [surface]. To manually render depth shadows for advanced use-cases, + * see the [depthEffect] [Modifier]. + * + * @property layer1 the 'base' [Shadow] layer, drawn first + * @property layer2 the second [Shadow] layer, drawn on top of [layer1] + */ +@Immutable +public class DepthEffect(public val layer1: Shadow, public val layer2: Shadow) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DepthEffect) return false + + if (layer1 != other.layer1) return false + if (layer2 != other.layer2) return false + + return true + } + + override fun hashCode(): Int { + var result = layer1.hashCode() + result = 31 * result + layer2.hashCode() + return result + } +} + +/** + * Renders shadows for the provided [depthEffect]. + * + * @param depthEffect Depth effect to render shadows for. If `null`, no shadows will be rendered. + * @param shape [Shape] of the shadows + */ +public fun Modifier.depthEffect(depthEffect: DepthEffect?, shape: Shape): Modifier { + if (depthEffect == null) return this + return this then DepthEffectElement(depthEffect, shape) +} + +/** + * Renders depth effect shadows by lerping between the provided [from] and [to] depth effects using + * [progress]. This allows for efficient animation - to render a static depth effect, see the other + * overload. + * + * @param from Depth effect to render shadows for when [progress] is 0. + * @param to Depth effect to render shadows for when [progress] is 1. + * @param shape [Shape] of the shadows + * @param progress progress of the animation between [from] and [to], from 0 to 1. Values may go + * outside these bounds for overshoot / undershoot. + */ +// TODO: can be simplified with style API in the future +internal fun Modifier.depthEffect( + from: DepthEffect?, + to: DepthEffect?, + shape: Shape, + progress: () -> Float, +): Modifier { + // dropShadow draws the shadow, and then the content on top. So in order to get layer2 to + // render on top of layer1, we draw layer1 first - this means that layer1's dropShadow will + // draw its shadow, and then its content on top (since the 'content' includes children + // modifiers, this includes layer2's shadow). + return this.dropShadow(shape) { + val shadow = lerp(from?.layer1, to?.layer1, progress()) + if (shadow != null) { + updateFrom(shadow) + } + } + .dropShadow(shape) { + val shadow = lerp(from?.layer2, to?.layer2, progress()) + if (shadow != null) { + updateFrom(shadow) + } + } +} + +private fun ShadowScope.updateFrom(shadow: Shadow) { + this.radius = shadow.radius.toPx() + this.spread = shadow.spread.toPx() + this.offset = Offset(shadow.offset.x.toPx(), shadow.offset.y.toPx()) + this.color = shadow.color + this.brush = shadow.brush + this.alpha = shadow.alpha + this.blendMode = shadow.blendMode +} + +private class DepthEffectElement(private val depthEffect: DepthEffect, private val shape: Shape) : + ModifierNodeElement() { + + override fun create(): DepthEffectNode = DepthEffectNode(depthEffect, shape) + + override fun update(node: DepthEffectNode) { + node.update(depthEffect, shape) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DepthEffectElement) return false + + if (depthEffect != other.depthEffect) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = depthEffect.hashCode() + result = 31 * result + shape.hashCode() + return result + } + + override fun InspectorInfo.inspectableProperties() { + name = "depthEffect" + properties["depthEffect"] = depthEffect + properties["shape"] = shape + } +} + +/** + * Renders shadows for the provided [shape] and [depthEffect]. + * + * @param depthEffect Depth effect to render shadows for. + * @param shape [Shape] of the shadows. + */ +internal class DepthEffectNode(private var depthEffect: DepthEffect, private var shape: Shape) : + Modifier.Node(), DrawModifierNode { + + private var layer1ShadowPainter: DropShadowPainter? = null + private var layer2ShadowPainter: DropShadowPainter? = null + + fun update(depthEffect: DepthEffect, shape: Shape) { + if (this.depthEffect.layer1 != depthEffect.layer1) layer1ShadowPainter = null + if (this.depthEffect.layer2 != depthEffect.layer2) layer2ShadowPainter = null + if (this.shape != shape) { + layer1ShadowPainter = null + layer2ShadowPainter = null + } + this.shape = shape + this.depthEffect = depthEffect + } + + override fun ContentDrawScope.draw() { + drawDepthEffect() + drawContent() + } + + internal fun ContentDrawScope.drawDepthEffect(alpha: Float = DefaultAlpha) { + // In order to get layer2 to render on top of layer1, we draw layer1 first. + with(obtainLayer1ShadowPainter()) { draw(size, alpha = alpha) } + with(obtainLayer2ShadowPainter()) { draw(size, alpha = alpha) } + } + + private fun obtainLayer1ShadowPainter(): DropShadowPainter = + layer1ShadowPainter + ?: requireGraphicsContext() + .shadowContext + .createDropShadowPainter(shape, depthEffect.layer1) + .also { layer1ShadowPainter = it } + + private fun obtainLayer2ShadowPainter(): DropShadowPainter = + layer2ShadowPainter + ?: requireGraphicsContext() + .shadowContext + .createDropShadowPainter(shape, depthEffect.layer2) + .also { layer2ShadowPainter = it } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md new file mode 100644 index 0000000..2985dd3 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md @@ -0,0 +1,105 @@ +When simulating depth on AI glasses using shadows, refer to the following source +code in `DepthEffectLevels.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.unit.dp + +/** + * Jetpack Compose Glimmer components can use [DepthEffect] to establish a sense of hierarchy. + * [DepthEffectLevels] contains different levels of [DepthEffect] to express this hierarchy. Higher + * levels contain larger shadows, and represent components with a higher z-order than lower levels. + * In their baseline state (not focused) most components should have no (`null`) [DepthEffect]. + * + * @property level1 the lowest level of [DepthEffect]. This level will have the smallest shadows. + * @property level2 a level of [DepthEffect] higher than [level1] and lower than [level3]. + * @property level3 a level of [DepthEffect] higher than [level2] and lower than [level4]. + * @property level4 a level of [DepthEffect] higher than [level3] and lower than [level5]. + * @property level5 the highest level of [DepthEffect]. This level will have the largest shadows. + * @see DepthEffect + * @see depthEffect + */ +@Immutable +public class DepthEffectLevels( + public val level1: DepthEffect = DepthEffectLevel1, + public val level2: DepthEffect = DepthEffectLevel2, + public val level3: DepthEffect = DepthEffectLevel3, + public val level4: DepthEffect = DepthEffectLevel4, + public val level5: DepthEffect = DepthEffectLevel5, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DepthEffectLevels) return false + + if (level1 != other.level1) return false + if (level2 != other.level2) return false + if (level3 != other.level3) return false + if (level4 != other.level4) return false + if (level5 != other.level5) return false + + return true + } + + override fun hashCode(): Int { + var result = level1.hashCode() + result = 31 * result + level2.hashCode() + result = 31 * result + level3.hashCode() + result = 31 * result + level4.hashCode() + result = 31 * result + level5.hashCode() + return result + } + + override fun toString(): String { + return "DepthEffectLevels(level1=$level1, level2=$level2, level3=$level3, level4=$level4, level5=$level5)" + } +} + +private val DepthEffectLevel1 = + DepthEffect( + layer1 = Shadow(radius = 12.dp, color = Color.Black, spread = 6.dp, alpha = 0.90f), + layer2 = Shadow(radius = 6.dp, color = Color.Black, spread = 2.dp), + ) +private val DepthEffectLevel2 = + DepthEffect( + layer1 = Shadow(radius = 23.dp, color = Color.Black, spread = 13.dp, alpha = 0.90f), + layer2 = Shadow(radius = 8.dp, color = Color.Black, spread = 5.dp), + ) +private val DepthEffectLevel3 = + DepthEffect( + layer1 = Shadow(radius = 34.dp, color = Color.Black, spread = 19.dp, alpha = 0.90f), + layer2 = Shadow(radius = 9.dp, color = Color.Black, spread = 7.dp), + ) +private val DepthEffectLevel4 = + DepthEffect( + layer1 = Shadow(radius = 45.dp, color = Color.Black, spread = 26.dp, alpha = 0.90f), + layer2 = Shadow(radius = 11.dp, color = Color.Black, spread = 10.dp), + ) +private val DepthEffectLevel5 = + DepthEffect( + layer1 = Shadow(radius = 56.dp, color = Color.Black, spread = 32.dp, alpha = 0.90f), + layer2 = Shadow(radius = 12.dp, color = Color.Black, spread = 12.dp), + ) +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md new file mode 100644 index 0000000..7461a2d --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md @@ -0,0 +1,270 @@ +When creating a `Typography` instance with the Google Sans Flex configuration, +refer to the following source code in `GoogleSansFlexTypography.kt`: + + +```kotlin +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("MentionsGoogle") + +package androidx.xr.glimmer.googlefonts + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.googlefonts.Font +import androidx.compose.ui.text.googlefonts.GoogleFont +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.sp +import androidx.xr.glimmer.Typography + +/** + * Returns a [Typography] instance configured with Google Sans Flex and customized with the + * recommended settings for Jetpack Compose Glimmer. + * + * Note: Google Sans Flex is a variable font. It is recommended to configure attributes such as + * weight, slant, and grade by using [FontVariation.Settings] (e.g. [FontVariation.weight]) rather + * than the [TextStyle] properties. + * + * @sample androidx.xr.glimmer.googlefonts.samples.GoogleSansFlexTypographySample + * @param titleLarge the [TextStyle] for [Typography.titleLarge] + * @param titleLargeVariationSettings the [FontVariation.Settings] for [Typography.titleLarge]. + * @param titleMedium the [TextStyle] for [Typography.titleMedium]. + * @param titleMediumVariationSettings the [FontVariation.Settings] for [Typography.titleMedium]. + * @param titleSmall the [TextStyle] for [Typography.titleSmall]. + * @param titleSmallVariationSettings the [FontVariation.Settings] for [Typography.titleSmall]. + * @param bodyLarge the [TextStyle] for [Typography.bodyLarge]. + * @param bodyLargeVariationSettings the [FontVariation.Settings] for [Typography.bodyLarge]. + * @param bodyMedium the [TextStyle] for [Typography.bodyMedium]. + * @param bodyMediumVariationSettings the [FontVariation.Settings] for [Typography.bodyMedium]. + * @param bodySmall the [TextStyle] for [Typography.bodySmall]. + * @param bodySmallVariationSettings the [FontVariation.Settings] for [Typography.bodySmall]. + * @param caption the [TextStyle] for [Typography.caption]. + * @param captionVariationSettings the [FontVariation.Settings] for [Typography.caption]. + */ +@Suppress("MentionsGoogle") +public fun createGoogleSansFlexTypography( + titleLarge: TextStyle = GoogleSansFlexTypographyDefaults.TitleLarge, + titleLargeVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.TitleLargeVariationSettings, + titleMedium: TextStyle = GoogleSansFlexTypographyDefaults.TitleMedium, + titleMediumVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.TitleMediumVariationSettings, + titleSmall: TextStyle = GoogleSansFlexTypographyDefaults.TitleSmall, + titleSmallVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.TitleSmallVariationSettings, + bodyLarge: TextStyle = GoogleSansFlexTypographyDefaults.BodyLarge, + bodyLargeVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.BodyLargeVariationSettings, + bodyMedium: TextStyle = GoogleSansFlexTypographyDefaults.BodyMedium, + bodyMediumVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.BodyMediumVariationSettings, + bodySmall: TextStyle = GoogleSansFlexTypographyDefaults.BodySmall, + bodySmallVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.BodySmallVariationSettings, + caption: TextStyle = GoogleSansFlexTypographyDefaults.Caption, + captionVariationSettings: FontVariation.Settings = + GoogleSansFlexTypographyDefaults.CaptionVariationSettings, +): Typography { + val titleLargeFontFamily = FontFamily(GoogleSansFlexFont, titleLargeVariationSettings) + val titleMediumFontFamily = FontFamily(GoogleSansFlexFont, titleMediumVariationSettings) + val titleSmallFontFamily = FontFamily(GoogleSansFlexFont, titleSmallVariationSettings) + val bodyLargeFontFamily = FontFamily(GoogleSansFlexFont, bodyLargeVariationSettings) + val bodyMediumFontFamily = FontFamily(GoogleSansFlexFont, bodyMediumVariationSettings) + val bodySmallFontFamily = FontFamily(GoogleSansFlexFont, bodySmallVariationSettings) + val captionFontFamily = FontFamily(GoogleSansFlexFont, captionVariationSettings) + + return Typography( + titleLarge = titleLarge.copy(fontFamily = titleLargeFontFamily), + titleMedium = titleMedium.copy(fontFamily = titleMediumFontFamily), + titleSmall = titleSmall.copy(fontFamily = titleSmallFontFamily), + bodyLarge = bodyLarge.copy(fontFamily = bodyLargeFontFamily), + bodyMedium = bodyMedium.copy(fontFamily = bodyMediumFontFamily), + bodySmall = bodySmall.copy(fontFamily = bodySmallFontFamily), + caption = caption.copy(fontFamily = captionFontFamily), + ) +} + +/** Default variable font configurations for using Google Sans Flex in Jetpack Compose Glimmer */ +@Suppress("MentionsGoogle") +public object GoogleSansFlexTypographyDefaults { + private val RoundVariationSetting = FontVariation.Setting("ROND", 100.0f) + + /** Default [TextStyle] for [Typography.titleLarge]. */ + public val TitleLarge: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 30.sp, + lineHeight = 36.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.titleLarge]. */ + public val TitleLargeVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(750), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.titleMedium]. */ + public val TitleMedium: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 28.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.titleMedium]. */ + public val TitleMediumVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(750), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.titleSmall]. */ + public val TitleSmall: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.titleSmall]. */ + public val TitleSmallVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(750), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.bodyLarge]. */ + public val BodyLarge: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 30.sp, + lineHeight = 36.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.bodyLarge]. */ + public val BodyLargeVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(520), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.bodyMedium]. */ + public val BodyMedium: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 36.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.bodyMedium]. */ + public val BodyMediumVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(520), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.bodySmall]. */ + public val BodySmall: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.bodySmall]. */ + public val BodySmallVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(520), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) + + /** Default [TextStyle] for [Typography.caption]. */ + public val Caption: TextStyle = + TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = DefaultLetterSpacing, + lineHeightStyle = DefaultLineHeightStyle, + ) + + /** Default [FontVariation.Settings] for [Typography.caption]. */ + public val CaptionVariationSettings: FontVariation.Settings = + FontVariation.Settings( + FontVariation.grade(0), + FontVariation.weight(650), + FontVariation.slant(0f), + FontVariation.width(100f), + FontVariation.opticalSizing(9.sp), + RoundVariationSetting, + ) +} + +private val GoogleSansFlexFont = GoogleFont("Google Sans Flex") + +private val DefaultLetterSpacing = 0.sp + +private val DefaultLineHeightStyle: LineHeightStyle = + LineHeightStyle( + alignment = LineHeightStyle.Alignment.Proportional, + trim = LineHeightStyle.Trim.FirstLineTop, + ) + +private fun FontFamily(font: GoogleFont, variationSettings: FontVariation.Settings): FontFamily = + FontFamily(Font(googleFont = font, variationSettings = variationSettings)) +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md new file mode 100644 index 0000000..325f09a --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md @@ -0,0 +1,182 @@ +When implementing Glimmer styles, refer to the following source code in +`GlimmerTheme.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.DelegatableNode +import androidx.xr.glimmer.GlimmerTheme.Companion.colors +import androidx.xr.glimmer.GlimmerTheme.Companion.depthEffectLevels +import androidx.xr.glimmer.GlimmerTheme.Companion.iconSizes +import androidx.xr.glimmer.GlimmerTheme.Companion.shapes + +/** + * Jetpack Compose Glimmer contains different theme subsystems to allow visual customization across + * an application. + * + * Components use properties provided here when retrieving default values. + * + * Any values that are not set will inherit the current value from the theme, falling back to the + * defaults if there is no parent GlimmerTheme. This allows using a GlimmerTheme at the top of your + * application, and then separate GlimmerTheme(s) for different screens / parts of your UI, + * overriding only the parts of the theme definition that need to change. + * + * @param colors [Colors] used by components within this hierarchy + * @param typography [Typography] used by components within this hierarchy + * @param componentSpacingValues [ComponentSpacingValues] used by components within this hierarchy + * @param content The content that can retrieve values from this theme + */ +@Composable +public fun GlimmerTheme( + colors: Colors = GlimmerTheme.colors, + typography: Typography = GlimmerTheme.typography, + componentSpacingValues: ComponentSpacingValues = GlimmerTheme.componentSpacingValues, + content: @Composable () -> Unit, +) { + val theme = GlimmerTheme(colors, typography, componentSpacingValues) + CompositionLocalProvider( + _localGlimmerTheme provides theme, + // TODO: b/413429405 + LocalIndication provides NoIndication, + LocalTextStyle provides typography.bodySmall, + LocalIconSize provides theme.iconSizes.medium, + content = content, + ) +} + +/** + * Jetpack Compose Glimmer contains different theme subsystems to allow visual customization across + * an application. + * + * Components use properties provided here when retrieving default values. + * + * @property colors [Colors] used by Jetpack Compose Glimmer components + * @property typography [Typography] used by Jetpack Compose Glimmer components. It is recommended + * to use `createGoogleSansFlexTypography()` from `androidx.xr.glimmer:glimmer-google-fonts` to + * create this. + * @property componentSpacingValues [ComponentSpacingValues] used by Jetpack Compose Glimmer + * components + * @property shapes [Shapes] used by Jetpack Compose Glimmer components + * @property depthEffectLevels [DepthEffectLevels] used by Jetpack Compose Glimmer components + * @property iconSizes [IconSizes] used by icons + */ +@Immutable +public class GlimmerTheme( + public val colors: Colors = Colors(), + public val typography: Typography = Typography(), + public val componentSpacingValues: ComponentSpacingValues = ComponentSpacingValues(), +) { + public val shapes: Shapes = _shapes + public val depthEffectLevels: DepthEffectLevels = _depthEffectLevels + public val iconSizes: IconSizes = _iconSizes + + public companion object { + /** Retrieves the current [Colors] at the call site's position in the hierarchy. */ + public val colors: Colors + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.colors + + /** Retrieves the current [Typography] at the call site's position in the hierarchy. */ + public val typography: Typography + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.typography + + /** + * Retrieves the current [ComponentSpacingValues] at the call site's position in the + * hierarchy. + */ + public val componentSpacingValues: ComponentSpacingValues + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.componentSpacingValues + + /** Retrieves the current [Shapes] at the call site's position in the hierarchy. */ + public val shapes: Shapes + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.shapes + + /** + * Retrieves the current [DepthEffectLevels] at the call site's position in the hierarchy. + */ + public val depthEffectLevels: DepthEffectLevels + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.depthEffectLevels + + /** Retrieves the current [IconSizes] at the call site's position in the hierarchy. */ + public val iconSizes: IconSizes + @Composable @ReadOnlyComposable get() = LocalGlimmerTheme.current.iconSizes + + /** + * [CompositionLocal] providing [GlimmerTheme] throughout the hierarchy. You can use + * properties in the companion object to access specific subsystems, for example [colors]. + * To provide a new value for this, use [GlimmerTheme]. This API is exposed to allow + * retrieving values from inside CompositionLocalConsumerModifierNode implementations - in + * most cases you should use [colors] and other properties directly. + */ + public val LocalGlimmerTheme: CompositionLocal + get() = _localGlimmerTheme + + /** + * Cached Shapes instance to be used across [GlimmerTheme] instances - currently shapes are + * not user-configurable. + */ + private val _shapes = Shapes() + + /** + * Cached [DepthEffectLevels] instance to be used across [GlimmerTheme] instances - + * currently depth effect levels are not user-configurable. + */ + private val _depthEffectLevels = DepthEffectLevels() + + /** + * Cached IconSizes instance to be used across [GlimmerTheme] instances - currently icon + * sizes are not user-configurable. + */ + internal val _iconSizes = IconSizes() + } + + internal var defaultSurfaceBorderCached: BorderStroke? = null +} + +private object NoIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return object : Modifier.Node() {} + } + + override fun equals(other: Any?): Boolean = other === this + + override fun hashCode(): Int = -5 +} + +/** Use [GlimmerTheme.LocalGlimmerTheme] to access this publicly. */ +@Suppress("CompositionLocalNaming") +private val _localGlimmerTheme: ProvidableCompositionLocal = + staticCompositionLocalOf { + GlimmerTheme() + } +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md new file mode 100644 index 0000000..1b8ad16 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md @@ -0,0 +1,389 @@ +When creating a Glimmer Icon component, refer to the following source code in +`Icon.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.structuralEqualityPolicy +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.CacheDrawModifierNode +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toolingGraphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp + +/** + * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For + * generic images that should not be tinted, and should not use the provided icon size, use the + * generic [androidx.compose.foundation.Image] instead. + * + * @param imageVector [ImageVector] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + imageVector: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = rememberVectorPainter(imageVector), + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a + * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so + * that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. Use the other overload of Icon without a [tint] parameter to apply the + * recommended content color provided by a [surface]. For multicolored icons and icons that should + * not be tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, + * and should not use the provided icon size, use the generic [androidx.compose.foundation.Image] + * instead. + * + * @param imageVector [ImageVector] to draw inside this icon + * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no tint + * is applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + imageVector: ImageVector, + tint: Color, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = rememberVectorPainter(imageVector), + tint = + if (tint.isSpecified) { + { tint } + } else null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For + * generic images that should not be tinted, and should not use the provided icon size, use the + * generic [androidx.compose.foundation.Image] instead. + * + * @param bitmap [ImageBitmap] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon(bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier) { + val painter = remember(bitmap) { BitmapPainter(bitmap) } + Icon(painter = painter, contentDescription = contentDescription, modifier = modifier) +} + +/** + * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a tint + * of [tint]. Icon is an opinionated component designed to be used with single-color icons so that + * they can be tinted correctly for the component they are placed in. The recommended icon sizes can + * be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly using [size], + * and components can use [LocalIconSize] to set the preferred size for any Icons inside the + * component. Use the other overload of Icon without a [tint] parameter to apply the recommended + * content color provided by a [surface]. For multicolored icons and icons that should not be + * tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, and + * should not use the provided icon size, use the generic [androidx.compose.foundation.Image] + * instead. + * + * @param bitmap [ImageBitmap] to draw inside this icon + * @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no tint is + * applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + bitmap: ImageBitmap, + tint: Color, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + val painter = remember(bitmap) { BitmapPainter(bitmap) } + Icon( + painter = painter, + tint = + if (tint.isSpecified) { + { tint } + } else null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set `null` for the tint parameter. For generic + * images that should not be tinted, and should not use the provided icon size, use the generic + * [androidx.compose.foundation.Image] instead. + * + * @param painter [Painter] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon(painter: Painter, contentDescription: String?, modifier: Modifier = Modifier) { + Icon( + painter = painter, + useContentColor = true, + tint = null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a + * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so + * that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. Use the other overload of Icon without a [tint] parameter to apply the + * recommended content color provided by a [surface]. For multicolored icons and icons that should + * not be tinted, set [tint] to `null`. For generic images that should not be tinted, and should not + * use the provided icon size, use the generic [androidx.compose.foundation.Image] instead. + * + * @param painter [Painter] to draw inside this icon + * @param tint tint to be applied to [painter]. If null, then no tint is applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + painter: Painter, + tint: ColorProducer?, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = painter, + useContentColor = false, + tint = tint, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * CompositionLocal containing the preferred size of an icon. This value will be used by [Icon] by + * default - it can be overridden with a [size] modifier. + */ +public val LocalIconSize: ProvidableCompositionLocal = + compositionLocalOf(structuralEqualityPolicy()) { GlimmerTheme._iconSizes.medium } + +@Composable +private fun Icon( + painter: Painter, + useContentColor: Boolean, + tint: ColorProducer?, + contentDescription: String?, + modifier: Modifier, +) { + val colorFilter = + if (useContentColor || tint != null) { + IconColorFilterElement( + useContentColor = useContentColor, + tint = tint, + painter = painter, + ) + } else { + Modifier + } + val semantics = + if (contentDescription != null) { + Modifier.semantics { + this.contentDescription = contentDescription + this.role = Role.Image + } + } else { + Modifier + } + Box( + modifier + .toolingGraphicsLayer() + .size(LocalIconSize.current) + .then(colorFilter) + .paint(painter, contentScale = ContentScale.Fit) + .then(semantics) + ) +} + +@Suppress("ModifierNodeInspectableProperties") +private class IconColorFilterElement( + private val useContentColor: Boolean, + private val tint: ColorProducer?, + private val painter: Painter, +) : ModifierNodeElement() { + override fun create() = IconColorFilterNode(useContentColor, tint, painter) + + override fun update(node: IconColorFilterNode) { + node.update(useContentColor, tint, painter) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IconColorFilterElement) return false + + if (useContentColor != other.useContentColor) return false + if (tint != other.tint) return false + if (painter != other.painter) return false + + return true + } + + override fun hashCode(): Int { + var result = useContentColor.hashCode() + result = 31 * result + (tint?.hashCode() ?: 0) + result = 31 * result + painter.hashCode() + return result + } +} + +private class IconColorFilterNode( + private var useContentColor: Boolean, + private var tint: ColorProducer?, + private var painter: Painter, +) : DelegatingNode() { + val cacheDrawNode = + delegate( + CacheDrawModifierNode { + val layer = obtainGraphicsLayer() + layer.apply { record { drawContent() } } + var cachedTintColor = Color.Unspecified + var cachedColorFilter: ColorFilter? = null + onDrawWithContent { + val tintColor = + if (useContentColor) { + currentContentColor() + } else { + tint?.invoke() ?: Color.Unspecified + } + if (cachedTintColor != tintColor) { + cachedTintColor = tintColor + cachedColorFilter = + if (tintColor.isSpecified) ColorFilter.tint(tintColor) else null + layer.colorFilter = cachedColorFilter + } + drawLayer(graphicsLayer = layer) + } + } + ) + + fun update(useContentColor: Boolean, tint: ColorProducer?, painter: Painter) { + val painterChanged = this.painter != painter + if (this.useContentColor != useContentColor || this.tint != tint || painterChanged) { + this.useContentColor = useContentColor + this.tint = tint + this.painter = painter + if (painterChanged) { + cacheDrawNode.invalidateDrawCache() + } else { + cacheDrawNode.invalidateDraw() + } + } + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md new file mode 100644 index 0000000..35e522f --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md @@ -0,0 +1,127 @@ +When creating a Glimmer Icon component, refer to the following source code in +`IconButton.kt` for icon buttons: + + +```kotlin +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * IconButton is a component used for exposing supplementary actions with a single tap. + * + * Icon buttons are used when a compact button is required, such as in a toolbar or image list. + * + * [content] should typically be an [Icon]. If using a custom icon, note that the typical size for + * the internal icon is 32 x 32 dp. Container has an overall minimum size of 48 x 48 dp. + * + * @param onClick called when this icon button is clicked + * @param modifier the [Modifier] to be applied to this icon button + * @param enabled controls the enabled state of this icon button. When `false`, this icon button + * will not respond to user input + * @param shape the [Shape] used to clip this icon button + * @param color background color of this icon button + * @param contentColor content color used by components inside [content] + * @param border the border to draw around this icon button + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this icon button. You can use this to change the icon button's + * appearance or preview the icon button in different states. Note that if `null` is provided, + * interactions will still happen internally. + * @param content the content of this icon button, typically an [Icon] + * @sample androidx.xr.glimmer.samples.IconButtonSample + */ +@Composable +public fun IconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = GlimmerTheme.shapes.large, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = IconButtonDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + val depthEffect = + SurfaceDepthEffect( + depthEffect = null, + focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1, + ) + Box( + modifier + .semantics { role = Role.Button } + .surface( + enabled = enabled, + shape = shape, + color = color, + contentColor = contentColor, + depthEffect = depthEffect, + border = border, + interactionSource = interactionSource, + onClick = onClick, + ) + .defaultMinSize(IconButtonDefaults.minimumSize) + .padding(contentPadding), + contentAlignment = Alignment.Center, + ) { + CompositionLocalProvider( + LocalIconSize provides IconButtonDefaults.iconSize, + content = content, + ) + } +} + +/** Default values used for [IconButton]. */ +public object IconButtonDefaults { + /** Default content padding for an [IconButton]. */ + @get:Composable + public val contentPadding: PaddingValues + get() = PaddingValues(GlimmerTheme.componentSpacingValues.small) + + /** Minimum size for icon-only buttons: [IconButton], [IconToggleButton]. */ + internal val minimumSize: Dp + get() = 48.dp + + /** Default icon size for icon-only buttons: [IconButton], [IconToggleButton]. */ + @get:Composable + internal val iconSize: Dp + get() = GlimmerTheme.iconSizes.small +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md new file mode 100644 index 0000000..9161001 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md @@ -0,0 +1,78 @@ +When creating a Glimmer Icon component, refer to the following source code in +`IconSizes.kt` for setting icon sizes: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * A set of named icon sizes. + * + * Sizes can be provided using [LocalIconSize] to set the size for [Icon]s within a component, or + * they can be set explicitly using [androidx.compose.foundation.layout.size]. + * + * @property small the size of a small icon. + * @property medium the size of a medium icon. This is the default icon size. + * @property large the size of a large icon. + * @see Icon + * @see LocalIconSize + */ +@Immutable +public class IconSizes( + public val small: Dp = 32.dp, + public val medium: Dp = 40.dp, + public val large: Dp = 48.dp, +) { + + /** Returns a copy of this IconSizes, optionally overriding some of the values. */ + public fun copy( + small: Dp = this.small, + medium: Dp = this.medium, + large: Dp = this.large, + ): IconSizes = IconSizes(small = small, medium = medium, large = large) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IconSizes) return false + + if (small != other.small) return false + if (medium != other.medium) return false + if (large != other.large) return false + + return true + } + + override fun hashCode(): Int { + var result = small.hashCode() + result = 31 * result + medium.hashCode() + result = 31 * result + large.hashCode() + return result + } + + override fun toString(): String { + return "IconSizes(small=$small, medium=$medium, large=$large)" + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md new file mode 100644 index 0000000..cc8400f --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md @@ -0,0 +1,394 @@ +When creating a Glimmer List component, refer to the following source code in +`List.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.list + +import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.layout.LazyLayout +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.foundation.scrollableArea +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap +import androidx.xr.glimmer.GlimmerTheme +import androidx.xr.glimmer.edgeScrim +import kotlin.math.max + +/** + * This is a scrolling list component that only composes and lays out the currently visible items. + * It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and + * customized behavior required for Jetpack Compose Glimmer. For Jetpack Compose Glimmer + * applications, it is recommended to use [VerticalList] instead of + * [androidx.compose.foundation.lazy.LazyColumn], as it is specifically designed to provide seamless + * focus-based navigation, visual scrim edge effects and support for focus-aware snap behavior. + * + * The [content] block defines a DSL which allows you to emit items of different types. For example, + * you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items. + * + * See the other [VerticalList] overload for a variant with a title slot. + * + * @sample androidx.xr.glimmer.samples.VerticalListSample + * @param modifier the modifier to apply to this layout. + * @param state the state object to be used to control or observe the list's state. + * @param contentPadding a padding around the whole content. This will add padding for the content + * after it has been clipped, which is not possible via [modifier] param. You can use it to add a + * padding before the first item or after the last one. + * @param userScrollEnabled If user gestures are enabled. + * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this + * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not + * need to use Modifier.overscroll separately. + * @param flingBehavior logic describing fling and snapping behavior when drag has finished. + * @param reverseLayout reverses the direction of scrolling and layout. + * @param horizontalAlignment aligns items horizontally. + * @param verticalArrangement is arrangement for items. This only applies if the content is smaller + * than the viewport. + * @param content a block which describes the content. Inside this block you can use methods like + * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. + */ +@Composable +public fun VerticalList( + modifier: Modifier = Modifier, + state: ListState = rememberListState(), + contentPadding: PaddingValues = VerticalListDefaults.contentPadding, + userScrollEnabled: Boolean = true, + overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), + flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state), + reverseLayout: Boolean = false, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement, + content: ListScope.() -> Unit, +): Unit = + List( + orientation = Orientation.Vertical, + modifier = modifier, + state = state, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + userScrollEnabled = userScrollEnabled, + overscrollEffect = overscrollEffect, + flingBehavior = flingBehavior, + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArrangement, + verticalAlignment = null, + horizontalArrangement = null, + content = content, + ) + +/** + * This is a scrolling list component that only composes and lays out the currently visible items. + * It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and + * customized behavior required for Jetpack Compose Glimmer. Jetpack Compose Glimmer applications + * should always use VerticalList instead of LazyColumn to ensure correct behavior. + * + * The [content] block defines a DSL which allows you to emit items of different types. For example, + * you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items. + * + * This overload of `VerticalList` contains a `title` slot. The title is expected to be a + * [androidx.xr.glimmer.TitleChip]. It is positioned at the top center and visually overlaps the + * list content. The list is vertically offset to start from the title's vertical center. When the + * list is scrolled, the title remains static. + * + * See the other [VerticalList] overload for a variant with no title slot. + * + * @sample androidx.xr.glimmer.samples.VerticalListWithTitleChipSample + * @param title a composable slot for the list title, expected to be a + * [androidx.xr.glimmer.TitleChip]. It overlaps the list, positioned at the top-center, and + * remains stuck to the top when the list is scrolled. + * @param modifier applies to the layout that contains both list and title. + * @param state the state object to be used to control or observe the list's state. + * @param contentPadding a padding around the whole content. This will add padding for the content + * after it has been clipped, which is not possible via [modifier] param. You can use it to add a + * padding before the first item or after the last one. The list is vertically offset to start + * from the title's vertical center, so custom content paddings must provide sufficient space to + * avoid content being obscured. + * @param userScrollEnabled If user gestures are enabled. + * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this + * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not + * need to use Modifier.overscroll separately. + * @param flingBehavior logic describing fling and snapping behavior when drag has finished. + * @param reverseLayout reverses the direction of scrolling and layout. + * @param horizontalAlignment aligns items horizontally. + * @param verticalArrangement is arrangement for items. This only applies if the content is smaller + * than the viewport. + * @param content a block which describes the content. Inside this block you can use methods like + * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. + */ +@Suppress( + // The main trailing lambda is [content], but it's DSL. + "ComposableLambdaParameterNaming", + "ComposableLambdaParameterPosition", +) +@Composable +public fun VerticalList( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + state: ListState = rememberListState(), + contentPadding: PaddingValues = VerticalListDefaults.contentPaddingWithTitle, + userScrollEnabled: Boolean = true, + overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), + flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state), + reverseLayout: Boolean = false, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement, + content: ListScope.() -> Unit, +) { + VerticalListWithTitleLayout( + modifier = modifier, + title = title, + list = { + VerticalList( + state = state, + contentPadding = contentPadding, + userScrollEnabled = userScrollEnabled, + overscrollEffect = overscrollEffect, + flingBehavior = flingBehavior, + reverseLayout = reverseLayout, + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArrangement, + content = content, + ) + }, + ) +} + +/** Contains the default values used by [VerticalList]. */ +public object VerticalListDefaults { + /** + * Recommended value for the distance between items. + * + * @see [verticalArrangement] for the default arrangement that uses this spacing. + */ + public val itemSpacing: Dp + @Composable get() = GlimmerTheme.componentSpacingValues.extraLarge + + /** The maximum height of the fade effects on the sides of the list. */ + public val ScrimMaxHeight: Dp = 46.dp + + /** Recommended content padding values for lists without a title. */ + public val contentPadding: PaddingValues + @Composable get() = PaddingValues(vertical = itemSpacing, horizontal = 0.dp) + + /** Recommended content padding values for lists with a title. */ + public val contentPaddingWithTitle: PaddingValues + @Composable get() = PaddingValues(top = ScrimMaxHeight, bottom = itemSpacing) + + /** Recommended values for the vertical arrangement. */ + public val verticalArrangement: Arrangement.Vertical + @Composable get() = Arrangement.spacedBy(itemSpacing) + + /** + * Creates and remembers the default fling behavior for a [VerticalList] that aligns the focus + * position with list scroll. + * + * @param state The [ListState] to observe for layout and focus information. + * @return A [FlingBehavior] instance that provides focus-aware snapping. + */ + @Composable + public fun flingBehavior(state: ListState): FlingBehavior { + val snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) } + return rememberSnapFlingBehavior(snapLayoutInfoProvider) + } +} + +@Composable +private fun VerticalListWithTitleLayout( + modifier: Modifier = Modifier, + title: @Composable () -> Unit, + list: @Composable () -> Unit, +) { + Layout(modifier = modifier, contents = listOf(list, title)) { measurables, constraints -> + // The title parameter is provided by users, requiring iteration through all measurables. + // The list parameter is provided by us, allowing to guarantee that it contains only + // a single measurable. + val listMeasurable = measurables[0][0] + val titleMeasurables = measurables[1] + // Measure title(s) first. + var titleMaxHeight = 0 + var titleMaxWidth = 0 + val titleConstraints = constraints.copyMaxDimensions() + val titlePlaceables = + titleMeasurables.fastMap { measurable -> + val placeable = measurable.measure(titleConstraints) + titleMaxHeight = max(titleMaxHeight, placeable.height) + titleMaxWidth = max(titleMaxWidth, placeable.width) + placeable + } + + // List shouldn't use the space above the vertical center of the title. + val titleYOffset = titleMaxHeight / 2 + val maxListHeight = constraints.maxHeight - titleYOffset + val minListHeight = minOf(constraints.minHeight, maxListHeight) + val listConstraints = constraints.copy(minHeight = minListHeight, maxHeight = maxListHeight) + val listPlaceable = listMeasurable.measure(listConstraints) + + val layoutWidth = maxOf(listPlaceable.width, titleMaxWidth) + val layoutHeight = listPlaceable.height + titleYOffset + layout(width = layoutWidth, height = layoutHeight) { + // Place the list first. + listPlaceable.placeRelative( + x = (layoutWidth - listPlaceable.width) / 2, + y = titleYOffset, + ) + // Then place the rest of the titles on top of the list. + titlePlaceables.fastForEach { titlePlaceable -> + // Each title's center aligned with the top of the list. + titlePlaceable.placeRelative( + x = (layoutWidth - titlePlaceable.width) / 2, + y = (titleMaxHeight - titlePlaceable.height) / 2, + ) + } + } + } +} + +/** + * The scrolling list that only composes and lays out the currently visible items. The [content] + * block defines a DSL which allows you to emit items of different types. For example, you can use + * [ListScope.item] to add a single item and [ListScope.items] to add a list of items. + * + * @param orientation The orientation in which to layout items in this list. + * @param modifier the modifier to apply to this layout. + * @param state the state object to be used to control or observe the list's state. + * @param contentPadding a padding around the whole content. This will add padding for the content + * after it has been clipped, which is not possible via [modifier] param. You can use it to add a + * padding before the first item or after the last one. + * @param userScrollEnabled If user gestures are enabled. + * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this + * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not + * need to use Modifier.overscroll separately. + * @param flingBehavior logic describing fling and snapping behavior when drag has finished. + * @param reverseLayout reverses the direction of scrolling and layout. + * @param horizontalAlignment aligns items horizontally. It's required and used only if + * [orientation] is [Orientation.Vertical]. + * @param verticalArrangement is arrangement for items. This only applies if the content is smaller + * than the viewport. It's required and used only if [orientation] is [Orientation.Vertical]. + * @param verticalAlignment aligns items vertically. It's required and used only if [orientation] is + * [Orientation.Horizontal]. + * @param horizontalArrangement is arrangement for items. This only applies if the content is + * smaller than the viewport. It's required and used only if [orientation] is + * [Orientation.Vertical]. + * @param content a block which describes the content. Inside this block you can use methods like + * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. + */ +@Composable +internal fun List( + orientation: Orientation, + modifier: Modifier, + state: ListState, + contentPadding: PaddingValues, + userScrollEnabled: Boolean, + overscrollEffect: OverscrollEffect?, + flingBehavior: FlingBehavior, + reverseLayout: Boolean, + horizontalAlignment: Alignment.Horizontal?, + verticalArrangement: Arrangement.Vertical?, + verticalAlignment: Alignment.Vertical?, + horizontalArrangement: Arrangement.Horizontal?, + content: ListScope.() -> Unit, +) { + val itemProvider = rememberGlimmerListItemProviderLambda(state, content) + + val semanticState = rememberGlimmerListSemanticState(state, orientation) + + val scrollEnabled = isScrollEnabled(userScrollEnabled, state) + + val measurePolicy = + rememberGlimmerListMeasurePolicy( + itemProviderLambda = itemProvider, + state = state, + contentPadding = contentPadding, + reverseLayout = reverseLayout, + orientation = orientation, + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArrangement, + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + ) + + val beyondBoundsModifier = + if (scrollEnabled) { + Modifier.lazyLayoutBeyondBoundsModifier( + state = rememberGlimmerListBeyondBoundsState(state), + beyondBoundsInfo = state.beyondBoundsInfo, + reverseLayout = reverseLayout, + orientation = orientation, + ) + } else { + Modifier + } + + LazyLayout( + modifier = + modifier + .then(state.remeasurementModifier) + .then(state.awaitLayoutModifier) + .autoFocus(state.autoFocusState) + .lazyLayoutSemantics( + itemProviderLambda = itemProvider, + state = semanticState, + orientation = orientation, + userScrollEnabled = scrollEnabled, + reverseScrolling = reverseLayout, + ) + .then(beyondBoundsModifier) + .edgeScrim( + state = state.scrollIndicatorState, + orientation = orientation, + maxScrimSize = VerticalListDefaults.ScrimMaxHeight, + ) + .scrollableArea( + state = state, + orientation = orientation, + enabled = scrollEnabled, + interactionSource = state.internalInteractionSource, + overscrollEffect = overscrollEffect, + flingBehavior = flingBehavior, + ), + itemProvider = itemProvider, + measurePolicy = measurePolicy, + ) +} + +@Composable +private fun isScrollEnabled(userScrollEnabled: Boolean, state: ListState): Boolean { + if (userScrollEnabled) { + val derivedState = + remember(state) { derivedStateOf { state.canScrollForward || state.canScrollBackward } } + return derivedState.value + } else { + return false + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md new file mode 100644 index 0000000..e1dba94 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md @@ -0,0 +1,282 @@ +When creating a Glimmer Icon Sizes component, refer to the following source +code in `ListItem.kt` for list items: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * ListItem is a component used to represent a single item in a + * [androidx.xr.glimmer.list.VerticalList]. A ListItem has a primary label [content], and may also + * have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * label is displayed below the primary label and can be used to provide additional information. A + * ListItem fills the maximum width available by default. + * + * This ListItem is focusable - see the other [ListItem] overload for a clickable ListItem. + * + * A simple ListItem with just a primary label: + * + * @sample androidx.xr.glimmer.samples.ListItemSample + * + * A ListItem with a primary and supporting label: + * + * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelSample + * + * A ListItem with a primary label, a supporting label, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelAndLeadingIconSample + * @param modifier the [Modifier] to be applied to this list item + * @param supportingLabel optional supporting label to be placed underneath the primary label + * [content] + * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is + * typically an [Icon]. + * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This + * is typically an [Icon]. + * @param shape the [Shape] used to clip this list item, and also used to draw the background and + * border + * @param color background color of this list item + * @param contentColor content color used by components inside [content], [supportingLabel], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this list item + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this list item. You can use this to change the list item's appearance + * or preview the list item in different states. Note that if `null` is provided, interactions + * will still happen internally. + * @param content the main content / primary label to display inside this list item + */ +@Composable +public fun ListItem( + modifier: Modifier = Modifier, + supportingLabel: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = ListItemDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + ListItemImpl( + modifier = modifier, + onClick = null, + supportingLabel = supportingLabel, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +/** + * ListItem is a component used to represent a single item in a + * [androidx.xr.glimmer.list.VerticalList]. A ListItem has a primary label [content], and may also + * have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * label is displayed below the primary label and can be used to provide additional information. A + * ListItem fills the maximum width available by default. + * + * This ListItem is focusable and clickable - see the other [ListItem] overload for a ListItem that + * is only focusable. + * + * A simple clickable ListItem with just a primary label: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemSample + * + * A clickable ListItem with a primary and supporting label: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelSample + * + * A clickable ListItem with a primary label, a supporting label, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelAndLeadingIconSample + * @param onClick called when this list item is clicked + * @param modifier the [Modifier] to be applied to this list item + * @param supportingLabel optional supporting label to be placed underneath the primary label + * [content] + * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is + * typically an [Icon]. + * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This + * is typically an [Icon]. + * @param shape the [Shape] used to clip this list item, and also used to draw the background and + * border + * @param color background color of this list item + * @param contentColor content color used by components inside [content], [supportingLabel], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this list item + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this list item. You can use this to change the list item's appearance + * or preview the list item in different states. Note that if `null` is provided, interactions + * will still happen internally. + * @param content the main content / primary label to display inside this list item + */ +@Composable +public fun ListItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, + supportingLabel: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = ListItemDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + ListItemImpl( + modifier = modifier, + onClick = onClick, + supportingLabel = supportingLabel, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +@Composable +private fun ListItemImpl( + modifier: Modifier, + onClick: (() -> Unit)?, + supportingLabel: @Composable (() -> Unit)?, + leadingIcon: @Composable (() -> Unit)?, + trailingIcon: @Composable (() -> Unit)?, + shape: Shape, + color: Color, + contentColor: Color, + border: BorderStroke?, + contentPadding: PaddingValues, + interactionSource: MutableInteractionSource?, + content: @Composable () -> Unit, +) { + val iconSize = GlimmerTheme.iconSizes.large + val typography = GlimmerTheme.typography + val innerPadding = GlimmerTheme.componentSpacingValues.small + val depthEffect = + SurfaceDepthEffect( + depthEffect = null, + focusedDepthEffect = GlimmerTheme.depthEffectLevels.level4, + ) + + val surfaceModifier = + if (onClick != null) { + Modifier.surface( + onClick = onClick, + shape = shape, + color = color, + contentColor = contentColor, + depthEffect = depthEffect, + border = border, + interactionSource = interactionSource, + ) + } else { + Modifier.surface( + shape = shape, + color = color, + contentColor = contentColor, + depthEffect = depthEffect, + border = border, + interactionSource = interactionSource, + ) + } + Row( + modifier = + modifier + .then(surfaceModifier) + .fillMaxWidth() + .defaultMinSize(minHeight = MinimumHeight) + .padding(contentPadding), + verticalAlignment = CenterVertically, + ) { + if (leadingIcon != null) { + Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopStart) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) + } + } + Column(Modifier.weight(1f).padding(horizontal = innerPadding)) { + if (supportingLabel == null) { + CompositionLocalProvider( + LocalTextStyle provides typography.bodySmall, + content = content, + ) + } else { + CompositionLocalProvider( + LocalTextStyle provides typography.titleSmall, + content = content, + ) + CompositionLocalProvider( + LocalTextStyle provides typography.bodySmall, + content = supportingLabel, + ) + } + } + if (trailingIcon != null) { + Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopEnd) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = trailingIcon) + } + } + } +} + +/** Default values used for [ListItem] */ +public object ListItemDefaults { + /** Default content padding used for a [ListItem] */ + public val contentPadding: PaddingValues + @Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.large) +} + +/** Default minimum height for a [ListItem] */ +private val MinimumHeight = 80.dp +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md new file mode 100644 index 0000000..134df7c --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md @@ -0,0 +1,420 @@ +When creating a Glimmer List component, refer to the following source code in +`ListState.kt` for creating a state for the list: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.list + +import androidx.annotation.IntRange +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.ScrollIndicatorState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList +import androidx.compose.runtime.Composable +import androidx.compose.runtime.annotation.FrequentlyChangingValue +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.Remeasurement +import androidx.compose.ui.layout.RemeasurementModifier +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.xr.glimmer.list.ListState.Companion.Saver +import kotlin.math.abs + +/** + * Creates a [ListState] that is remembered across compositions. + * + * Changes to the provided initial values will **not** result in the state being recreated or + * changed in any way if it has already been created. + * + * @param initialFirstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param initialFirstVisibleItemScrollOffset the initial value for + * [ListState.firstVisibleItemScrollOffset] + */ +@Composable +public fun rememberListState( + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Int = 0, +): ListState = + rememberSaveable(saver = ListState.Saver) { + ListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) + } + +/** + * A state object that can be hoisted to control and observe scrolling. + * + * In most cases, this will be created via [rememberListState]. + * + * @param firstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param firstVisibleItemScrollOffset the initial value for + * [ListState.firstVisibleItemScrollOffset] + */ +public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) : + ScrollableState { + + private val backingState = ScrollableState { -onScroll(-it) } + + // TODO: b/414961654 - Consider making this abstraction around "anchor item". + /** The holder class for the current scroll position. */ + private val scrollPosition = + GlimmerListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) + + /** Backing state for [layoutInfo] */ + internal val layoutInfoState = mutableStateOf(EmptyLazyListMeasureResult, neverEqualPolicy()) + + private val density: Density + get() = layoutInfoState.value.density + + /** + * This field is used to save information about the number of "beyond bounds items" that we want + * to compose. These items are not within the visible bounds of the lazy layout, but we compose + * them because they are explicitly requested through the + * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout]. + */ + internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() + + /** Includes information for requesting focus for children as the list scrolls. */ + internal val autoFocusState = GlimmerListAutoFocusState() + + /** Stores currently pinned items which are always composed. */ + internal val pinnedItems = LazyLayoutPinnedItemList() + + internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource() + + /** + * The scroll amount was provided in `onScroll` to be consumed during the current measure pass. + * + * Scrolling forward is negative. + */ + internal var incomingScroll: Float = 0f + private set + + /** + * This value retains the scroll amount that we want to carry over between measure passes. It + * represents the amount of scroll from previous passes that wasn't consumed back then but is + * awaiting to be consumed later. Together with [incomingScroll], this represents the total + * scroll available to the list during the current measure pass. + * + * This variable exists because the layout uses integer pixels while [onScroll] operates with + * fractional floats. Consequently, the list might receive tiny fractions of scroll input which, + * when aggregated, should equate to a few pixels of scrolling. However, because these small + * fractions are spread across many [onScroll] invocations, the list ignores them, making it + * feel unresponsive during slow, gentle touches. To avoid this, instead of ignoring these tiny + * scroll increments, the list accumulates them so they can be added to the next scroll part and + * actually consumed in a subsequent pass. + * + * Note that this value can sometimes be opposite to the scroll direction. This occurs because + * the [Float] -> [Int] rounding causes us to consume more than was provided. To compensate, we + * add a negative value to [incomingScroll] the next time. + * + * Scrolling forward is negative. + */ + internal var carryOverScroll: Float = 0f + private set + + /** + * This value is updated after the measure pass inside [applyMeasureResult] and defines how much + * of the [incomingScroll] was actually used. + */ + private var consumedScroll: Float = 0f + + internal val nearestRange: kotlin.ranges.IntRange by + LazyLayoutNearestRangeState(0, NearestItemsSlidingWindowSize, NearestItemsExtraItemCount) + + /** + * The [Remeasurement] object associated with our layout. It allows us to remeasure + * synchronously during scroll. + */ + internal var remeasurement: Remeasurement? = null + private set + + /** The modifier which provides [remeasurement]. */ + internal val remeasurementModifier = + object : RemeasurementModifier { + override fun onRemeasurementAvailable(remeasurement: Remeasurement) { + this@ListState.remeasurement = remeasurement + } + } + + /** + * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is + * ready. + */ + internal val awaitLayoutModifier = AwaitFirstLayoutModifier() + + /** + * The index of the first item that is visible within the scrollable viewport area not including + * items in the content padding region. For the first visible item that includes items in the + * content padding please use [ListLayoutInfo.visibleItemsInfo]. + * + * Note that this property is observable and if you use it in the composable function it will be + * recomposed on every change causing potential performance issues. + */ + public val firstVisibleItemIndex: Int + @FrequentlyChangingValue get() = scrollPosition.index + + /** + * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount + * that the item is offset backwards. + * + * Note that this property is observable and if you use it in the composable function it will be + * recomposed on every scroll causing potential performance issues. + */ + public val firstVisibleItemScrollOffset: Int + @FrequentlyChangingValue get() = scrollPosition.scrollOffset + + /** + * The object of [ListLayoutInfo] calculated during the last layout pass. For example, you can + * use it to calculate what items are currently visible. + * + * Note that this property is observable and is updated after every scroll or remeasure. If you + * use it in the composable function it will be recomposed on every change causing potential + * performance issues including infinity recomposition loop. Therefore, avoid using it in the + * composition. + * + * If you want to run some side effects like sending an analytics event or updating a state + * based on this value consider using "snapshotFlow": + */ + public val layoutInfo: ListLayoutInfo + @FrequentlyChangingValue get() = layoutInfoState.value + + /** + * [InteractionSource] that will be used to dispatch drag events when this list is being + * dragged. If you want to know whether the fling (or animated scroll) is in progress, use + * [isScrollInProgress]. + */ + public val interactionSource: InteractionSource + get() = internalInteractionSource + + /** Snaps to the requested scroll position. */ + internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) { + scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) + remeasurement?.forceRemeasure() + } + + /** + * When the user provided custom keys for the items we can try to detect when there were items + * added or removed before our current first visible item and keep this item as the first + * visible one even given that its index has been changed. + */ + internal fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: GlimmerListItemProvider, + firstItemIndex: Int, + ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) + + /** + * Called during the measurement pass once the dispatched [incomingScroll] has been handled and + * the new item positions are known. + * + * @param result lazy list measuring results. + * @param consumedScroll defines how much scroll was consumed during the measure pass. + * @param scrollToCarryOver tracks the amount of scrolling that the internal logic has reported + * as consumed, but wants to save for later measurement. Also, if the list consumes more + * scroll than it was given (for example, due to rounding errors), this value can be set to a + * negative amount to balance it out in the next pass. This value should never be larger than + * [consumedScroll]. + */ + internal fun applyMeasureResult( + result: GlimmerListMeasureResult, + consumedScroll: Float, + scrollToCarryOver: Float, + ) { + this.consumedScroll = consumedScroll + this.carryOverScroll = scrollToCarryOver + + canScrollBackward = result.canScrollBackward + canScrollForward = result.canScrollForward + layoutInfoState.value = result + + scrollPosition.updateFromMeasureResult(result) + } + + internal fun onScroll(distance: Float): Float { + if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { + return 0f + } + + // Fast path. Skip measure pass. + if (abs(distance + carryOverScroll) <= 0.5f) { + // Inside measuring we do `scrollToBeConsumed.roundToInt()` so there will be no scroll + // if we have less than 0.5 pixels. So just accumulate it for the next pass. + carryOverScroll += distance + return distance + } + + incomingScroll = distance + // The `forceRemeasure()` invocation triggers the measure pass where [incomingScroll] + // will be used to update [consumedScroll] and [carryOverScroll] values. + remeasurement?.forceRemeasure() + // It's important to reset this value because there are measure passes + // triggered from outside scrolling. They read this value as well. + // So, after we used it, we need to reset it to zero. + incomingScroll = 0f + + return consumedScroll + } + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + awaitLayoutModifier.waitForFirstLayout() + backingState.scroll(scrollPriority, block) + } + + override fun dispatchRawDelta(delta: Float): Float = backingState.dispatchRawDelta(delta) + + override val isScrollInProgress: Boolean + get() = backingState.isScrollInProgress + + @get:Suppress("GetterSetterNames") + override var canScrollForward: Boolean by mutableStateOf(false) + private set + + @get:Suppress("GetterSetterNames") + override var canScrollBackward: Boolean by mutableStateOf(false) + private set + + override val scrollIndicatorState: ScrollIndicatorState + get() = _scrollIndicatorState + + private val _scrollIndicatorState = + object : ScrollIndicatorState { + override val scrollOffset: Int + @FrequentlyChangingValue + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + this@ListState.firstVisibleItemIndex * visibleItemsAverageSize + + this@ListState.firstVisibleItemScrollOffset + } + } + + override val contentSize: Int + @FrequentlyChangingValue + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + // Approximate size of all content + (totalItemsCount * visibleItemsAverageSize) - + // Subtract the final trailing spacing that is not shown + (if (totalItemsCount > 0) mainAxisItemSpacing else 0) + + // Add the inner paddings (which are separate from the item sizes) + beforeContentPadding + + afterContentPadding + } + } + + override val viewportSize: Int + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + mainAxisViewportSize + } + } + } + + /** + * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset] + * pixels. + * + * @param index the index to which to scroll. Must be non-negative. + * @param scrollOffset the offset that the item should end up after the scroll. Note that + * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will + * scroll the item further upward (taking it partly offscreen). + */ + public suspend fun scrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { + scroll { snapToItemIndexInternal(index, scrollOffset) } + } + + /** + * Animate (smooth scroll) to the given item. + * + * @param index the index to which to scroll. Must be non-negative. + * @param scrollOffset the offset that the item should end up after the scroll. Note that + * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will + * scroll the item further upward (taking it partly offscreen). + */ + public suspend fun animateScrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { + scroll { + GlimmerListScrollScope(this@ListState, this) + .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density) + } + } + + public companion object { + /** The default [Saver] implementation for [ListState]. */ + public val Saver: Saver = + listSaver( + save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, + restore = { + ListState(firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1]) + }, + ) + } +} + +private val EmptyLazyListMeasureResult = + GlimmerListMeasureResult( + firstVisibleItem = null, + firstVisibleItemScrollOffset = 0, + canScrollForward = false, + consumedScroll = 0f, + measureResult = + object : MeasureResult { + override val width: Int = 0 + override val height: Int = 0 + + @Suppress("PrimitiveInCollection") + override val alignmentLines: Map = emptyMap() + + override fun placeChildren() {} + }, + visibleItemsInfo = emptyList(), + viewportStartOffset = 0, + viewportEndOffset = 0, + totalItemsCount = 0, + reverseLayout = false, + orientation = Orientation.Vertical, + afterContentPadding = 0, + mainAxisItemSpacing = 0, + remeasureNeeded = false, + density = Density(1f), + childConstraints = Constraints(), + ) +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md new file mode 100644 index 0000000..821566a --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md @@ -0,0 +1,217 @@ +Use the following calculation metrics for color contrast in `hct.ts`: + +```typescript +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A color system built using CAM16 hue and chroma, and L* from + * L*a*b*. + * + * Using L* creates a link between the color system, contrast, and thus + * accessibility. Contrast ratio depends on relative luminance, or Y in the XYZ + * color space. L*, or perceptual luminance can be calculated from Y. + * + * Unlike Y, L* is linear to human perception, allowing trivial creation of + * accurate color tones. + * + * Unlike contrast ratio, measuring contrast in L* is linear, and simple to + * calculate. A difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, + * and a difference of 50 guarantees a contrast ratio >= 4.5. + */ + +import * as utils from '../utils/color_utils.js'; + +import {Cam16} from './cam16.js'; +import {HctSolver} from './hct_solver.js'; +import {ViewingConditions} from './viewing_conditions.js'; + + +/** + * HCT, hue, chroma, and tone. A color system that provides a perceptually + * accurate color measurement system that can also accurately render what colors + * will appear as in different lighting environments. + */ +export class Hct { + /** + * @param hue 0 <= hue < 360; invalid values are corrected. + * @param chroma 0 <= chroma < ?; Informally, colorfulness. The color + * returned may be lower than the requested chroma. Chroma has a different + * maximum for any given hue and tone. + * @param tone 0 <= tone <= 100; invalid values are corrected. + * @return HCT representation of a color in default viewing conditions. + */ + + internalHue: number; + internalChroma: number; + internalTone: number; + + static from(hue: number, chroma: number, tone: number) { + return new Hct(HctSolver.solveToInt(hue, chroma, tone)); + } + + /** + * @param argb ARGB representation of a color. + * @return HCT representation of a color in default viewing conditions + */ + static fromInt(argb: number) { + return new Hct(argb); + } + + toInt(): number { + return this.argb; + } + + /** + * A number, in degrees, representing ex. red, orange, yellow, etc. + * Ranges from 0 <= hue < 360. + */ + get hue(): number { + return this.internalHue; + } + + /** + * @param newHue 0 <= newHue < 360; invalid values are corrected. + * Chroma may decrease because chroma has a different maximum for any given + * hue and tone. + */ + set hue(newHue: number) { + this.setInternalState( + HctSolver.solveToInt( + newHue, + this.internalChroma, + this.internalTone, + ), + ); + } + + get chroma(): number { + return this.internalChroma; + } + + /** + * @param newChroma 0 <= newChroma < ? + * Chroma may decrease because chroma has a different maximum for any given + * hue and tone. + */ + set chroma(newChroma: number) { + this.setInternalState( + HctSolver.solveToInt( + this.internalHue, + newChroma, + this.internalTone, + ), + ); + } + + /** Lightness. Ranges from 0 to 100. */ + get tone(): number { + return this.internalTone; + } + + /** + * @param newTone 0 <= newTone <= 100; invalid valids are corrected. + * Chroma may decrease because chroma has a different maximum for any given + * hue and tone. + */ + set tone(newTone: number) { + this.setInternalState( + HctSolver.solveToInt( + this.internalHue, + this.internalChroma, + newTone, + ), + ); + } + + /** Sets a property of the Hct object. */ + setValue(propertyName: string, value: number) { + (this as any)[propertyName] = value; + } + + toString(): string { + return `HCT(${this.hue.toFixed(0)}, ${this.chroma.toFixed(0)}, ${ + this.tone.toFixed(0)})`; + } + + static isBlue(hue: number): boolean { + return hue >= 250 && hue < 270; + } + + static isYellow(hue: number): boolean { + return hue >= 105 && hue < 125; + } + + static isCyan(hue: number): boolean { + return hue >= 170 && hue < 207; + } + + private constructor(private argb: number) { + const cam = Cam16.fromInt(argb); + this.internalHue = cam.hue; + this.internalChroma = cam.chroma; + this.internalTone = utils.lstarFromArgb(argb); + this.argb = argb; + } + + private setInternalState(argb: number) { + const cam = Cam16.fromInt(argb); + this.internalHue = cam.hue; + this.internalChroma = cam.chroma; + this.internalTone = utils.lstarFromArgb(argb); + this.argb = argb; + } + + /** + * Translates a color into different [ViewingConditions]. + * + * Colors change appearance. They look different with lights on versus off, + * the same color, as in hex code, on white looks different when on black. + * This is called color relativity, most famously explicated by Josef Albers + * in Interaction of Color. + * + * In color science, color appearance models can account for this and + * calculate the appearance of a color in different settings. HCT is based on + * CAM16, a color appearance model, and uses it to make these calculations. + * + * See [ViewingConditions.make] for parameters affecting color appearance. + */ + inViewingConditions(vc: ViewingConditions): Hct { + // 1. Use CAM16 to find XYZ coordinates of color in specified VC. + const cam = Cam16.fromInt(this.toInt()); + const viewedInVc = cam.xyzInViewingConditions(vc); + + // 2. Create CAM16 of those XYZ coordinates in default VC. + const recastInVc = Cam16.fromXyzInViewingConditions( + viewedInVc[0], + viewedInVc[1], + viewedInVc[2], + ViewingConditions.make(), + ); + + // 3. Create HCT from: + // - CAM16 using default VC with XYZ coordinates in specified VC. + // - L* converted from Y in XYZ coordinates in specified VC. + const recastHct = Hct.from( + recastInVc.hue, + recastInVc.chroma, + utils.lstarFromY(viewedInVc[1]), + ); + return recastHct; + } +} +``` \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md new file mode 100644 index 0000000..959ce2e --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md @@ -0,0 +1,265 @@ +When using a Projected Context to launch the Glasses Projected activity on the +Projected Device, refer to the following source code in `ProjectedContext.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.projected + +import android.app.ActivityOptions +import android.companion.virtual.VirtualDeviceManager +import android.content.Context +import android.hardware.display.DisplayManager +import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_ADDED +import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED +import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import androidx.xr.projected.experimental.ExperimentalProjectedApi +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** + * Helper for accessing Projected device [Context] and its features. + * + * Projected device is an XR device connected to an Android device (host). Host can project the + * application content to the Projected device and let users interact with it. + * + * The Projected device context will ensure Projected device system services are returned, when + * queried for system services from this object. + * + * Note: The application context's deviceId can switch between the Projected and host deviceId + * depending on which activity was most recently in the foreground. Prefer using the Activity + * context to minimize the risk of running into this problem. + */ +@ExperimentalProjectedApi +public object ProjectedContext { + + private const val TAG = "ProjectedContext" + + @VisibleForTesting internal const val PROJECTED_DEVICE_NAME = "ProjectionDevice" + @VisibleForTesting internal const val PROJECTED_DISPLAY_NAME = "ProjectionDisplay" + + /** + * Explicitly create the Projected device context from any context object. + * + * @throws IllegalStateException if the projected device was not found. + */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public fun createProjectedDeviceContext(context: Context): Context { + val deviceId = + getProjectedDeviceId(context) + ?: throw IllegalStateException("Projected device not found.") + return context.createDeviceContext(deviceId) + } + + /** + * Explicitly create the host device context from any context object. The host is the device + * that connects to a Projected device. + * + * If an application is using a Projected device context and it wants to use system services + * from the host (e.g. phone), it needs to use the host device context. + */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public fun createHostDeviceContext(context: Context): Context = + context.createDeviceContext(Context.DEVICE_ID_DEFAULT) + + /** + * Returns the name of the Projected device or null if either virtual device wasn't found or the + * name of the virtual device wasn't set. + * + * @throws IllegalArgumentException If another context is used (e.g. the host context). + */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public fun getProjectedDeviceName(context: Context): String? = + // TODO: b/424812882 - Turn this into a lint check with an annotation. + if (isProjectedDeviceContext(context)) { + getVirtualDevice(context)?.name + } else { + throw IllegalArgumentException( + "Provided context is not the Projected device context. Can't get the device name." + ) + } + + /** Returns whether the provided context is the Projected device context. */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public fun isProjectedDeviceContext(context: Context): Boolean = + getVirtualDevice(context)?.name?.startsWith(PROJECTED_DEVICE_NAME) == true + + /** + * Creates [ActivityOptions] that should be used to start an activity on the Projected device. + * + * @param context any [Context] object. If the provided context is not a Projected device + * context, a Projected device context will be created automatically and used to create + * Projected [ActivityOptions]. + * @throws IllegalStateException if the projected display was not found or if the Projected + * device doesn't have any displays. + */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + public fun createProjectedActivityOptions(context: Context): ActivityOptions { + val projectedDeviceContext = + if (isProjectedDeviceContext(context)) { + context + } else { + createProjectedDeviceContext(context) + } + + val projectedDisplayIds = getProjectedDisplayIds(projectedDeviceContext) + + check(projectedDisplayIds.isNotEmpty()) { "Projected device doesn't have any displays." } + + val projectedDisplay = + context.getSystemService(DisplayManager::class.java).displays.find { + it.name == PROJECTED_DISPLAY_NAME && projectedDisplayIds.contains(it.displayId) + } ?: throw IllegalStateException("No projected display found.") + + return ActivityOptions.makeBasic().setLaunchDisplayId(projectedDisplay.displayId) + } + + /** + * Observe whether a Projected device is connected to the host. + * + * @param context The context used to access the [VirtualDeviceManager]. It can be any context + * object. + * @param coroutineContext The CoroutineContext that includes CoroutineDispatcher which is where + * the Projected device connectivity observer is executed on. + * @throws IllegalArgumentException if provided coroutineContext doesn't include + * CoroutineDispatcher. + */ + @JvmStatic + @RequiresApi(Build.VERSION_CODES.BAKLAVA) + public fun isProjectedDeviceConnected( + context: Context, + coroutineContext: CoroutineContext, + ): Flow = + callbackFlow { + @OptIn(ExperimentalStdlibApi::class) + val coroutineDispatcher = + coroutineContext[CoroutineDispatcher] + ?: throw IllegalArgumentException( + "CoroutineContext must contain a CoroutineDispatcher." + ) + + fun checkAndSend() { + trySend(isProjectedDisplayAvailable(context)) + } + + val virtualDeviceListener = + object : VirtualDeviceManager.VirtualDeviceListener { + override fun onVirtualDeviceCreated(deviceId: Int) { + checkAndSend() + } + + override fun onVirtualDeviceClosed(deviceId: Int) { + checkAndSend() + } + } + + val displayListener = + object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) { + checkAndSend() + } + + override fun onDisplayChanged(displayId: Int) { + checkAndSend() + } + + override fun onDisplayRemoved(displayId: Int) { + checkAndSend() + } + } + + checkAndSend() + + val virtualDeviceManager = + context.getSystemService(VirtualDeviceManager::class.java) + virtualDeviceManager.registerVirtualDeviceListener( + coroutineDispatcher.asExecutor(), + virtualDeviceListener, + ) + + val displayManager = context.getSystemService(DisplayManager::class.java) + val eventFilter = + EVENT_TYPE_DISPLAY_ADDED or + EVENT_TYPE_DISPLAY_CHANGED or + EVENT_TYPE_DISPLAY_REMOVED + displayManager.registerDisplayListener( + coroutineDispatcher.asExecutor(), + eventFilter, + displayListener, + ) + + awaitClose { + virtualDeviceManager.unregisterVirtualDeviceListener(virtualDeviceListener) + displayManager.unregisterDisplayListener(displayListener) + } + } + .distinctUntilChanged() + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private fun isProjectedDisplayAvailable(context: Context): Boolean { + val projectedDeviceContext = + try { + createProjectedDeviceContext(context) + } catch (e: IllegalStateException) { + return false + } + val projectedDisplayIds = getProjectedDisplayIds(projectedDeviceContext) + if (projectedDisplayIds.isEmpty()) { + return false + } + val displayManager = context.getSystemService(DisplayManager::class.java) + return displayManager.displays.any { + it.name == PROJECTED_DISPLAY_NAME && projectedDisplayIds.contains(it.displayId) + } + } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private fun getProjectedDeviceId(context: Context) = + context + .getSystemService(VirtualDeviceManager::class.java) + .virtualDevices + // TODO: b/424824481 - Replace the name matching with a better method. + .find { it.name?.startsWith(PROJECTED_DEVICE_NAME) ?: false } + ?.deviceId + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private fun getVirtualDevice(context: Context) = + context.getSystemService(VirtualDeviceManager::class.java).virtualDevices.find { + it.deviceId == context.deviceId + } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private fun getProjectedDisplayIds(context: Context) = + getVirtualDevice(context)?.displayIds ?: IntArray(size = 0) +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md new file mode 100644 index 0000000..922a179 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md @@ -0,0 +1,368 @@ +When creating a Glimmer Stack component, refer to the following source code in +`Stack.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.stack + +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.referentialEqualityPolicy +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirstOrNull +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap +import androidx.compose.ui.util.lerp +import androidx.compose.ui.zIndex +import kotlin.math.roundToInt + +/** + * [VerticalStack] is a lazy, vertically scrollable layout that arranges its items in a visually + * overlapping, three-dimensional sequence, which resembles a deck of cards. The primary item is + * prominently displayed in the foreground. Subsequent items are positioned behind the primary item + * along the z-axis with a small portion of the next item revealed to indicate depth and upcoming + * content. + * + * As the user scrolls vertically, the active foreground item transitions out of view, allowing the + * item immediately beneath it to slide into the prominent foreground position. Items always + * snap-animate into the foreground position after the user's gesture ends. + * + * Note: When displaying text within a [VerticalStack], it is strongly recommended to set + * [androidx.compose.ui.text.TextStyle.textMotion] to + * [androidx.compose.ui.text.style.TextMotion.Animated]. This ensures smooth rendering during layout + * animations or scaling transitions, preventing pixel-snapping artifacts. + * + * @sample androidx.xr.glimmer.samples.VerticalStackSample + * @param modifier the modifier to apply to this layout. + * @param state the state of the stack. + * @param content a block that describes the content. Inside this block you can use methods like + * [StackScope.item] to add a single item or [StackScope.items] to add a collection of items. + */ +@Composable +public fun VerticalStack( + modifier: Modifier = Modifier, + state: StackState = rememberStackState(), + content: StackScope.() -> Unit, +) { + val latestContent = rememberUpdatedState(content) + val stackItemHolderState = + remember(state) { + // Re-run the DSL to parse items only when the content lambda instance changes. + derivedStateOf(referentialEqualityPolicy()) { + StackItemHolder(state, latestContent.value).also { + // Set the item count on the StackState immediately when the derived state + // re-evaluates (i.e., when content changes), even before recomposition. + state.itemCount = it.itemCount + } + } + .also { + // This second assignment is necessary to force the derivedStateOf lambda above + // (which is executed lazily) to execute synchronously inside this block. + state.itemCount = it.value.itemCount + } + } + + val singleItemScrollConstraintConnection = + remember(state.pagerState) { SingleItemScrollConstraintConnection(state.pagerState) } + + VerticalPager( + state = state.pagerState, + modifier = + modifier + .then(StackInitialFocusElement(state)) + .stackScrim() + .nestedScroll(singleItemScrollConstraintConnection), + contentPadding = PaddingValues(bottom = RevealAreaSize), + key = { page -> stackItemHolderState.value.getKey(page) }, + beyondViewportPageCount = MaxNextVisibleItemCount, + flingBehavior = + PagerDefaults.flingBehavior( + state = state.pagerState, + pagerSnapDistance = PagerSnapDistance.atMost(1), + snapAnimationSpec = SnapAnimationSpec, + ), + ) { page -> + val stackItemHolder = stackItemHolderState.value + stackItemHolder.withInterval(page) { localIndex, itemInterval -> + val key = + itemInterval.getKeyOrDefault(globalIndex = page, localIntervalIndex = localIndex) + val itemScope = itemInterval.getOrCreateItemScope(key) + itemScope.index = page + StackItemLayout(page = page, state = state, itemScope = itemScope) { + itemInterval.item(itemScope, localIndex) + } + } + } +} + +@Composable +private fun StackItemLayout( + page: Int, + state: StackState, + itemScope: StackItemScopeImpl, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val focusRequester = remember { FocusRequester() } + Layout( + content = content, + modifier = + modifier + .zIndex(-page.toFloat()) + .onPlaced { itemScope.coordinates = it } + .maskItemsBelow(state, itemScope) + .focusRequester(focusRequester) + .onFocusChanged { state.onItemFocusChanged(page, it) }, + ) { measurables, constraints -> + var maxWidth = 0 + var maxHeight = 0 + val placeables = + measurables.fastMap { + it.measure(constraints).also { placeable -> + maxWidth = maxOf(maxWidth, placeable.width) + maxHeight = maxOf(maxHeight, placeable.height) + } + } + + if (!isLookingAhead) { + state.layoutInfoInternal.updateMeasuredHeight(index = page, height = maxHeight) + } + + layout(maxWidth, maxHeight) { + placeables.fastForEach { placeable -> + placeable.placeRelativeWithLayer(x = 0, y = 0) { + val revealHeight = RevealAreaSize.roundToPx() + val topItem = state.topItem + when { + page.isTopItem(topItem = topItem) -> { + translationY = + state.topItemTranslationY( + revealHeight = revealHeight, + topItemHeight = placeable.height, + ) + + state.notifyAutoFocus(page, focusRequester) + } + + page.isNextItem(topItem = topItem) -> { + translationY = + state.nextItemTranslationY( + revealHeight = revealHeight, + topItem = topItem, + nextItemHeight = placeable.height, + ) + val scale = state.nextItemScale() + scaleX = scale + scaleY = scale + + state.notifyAutoFocus(page, focusRequester) + } + + page.isNextNextItem(topItem = topItem) -> { + translationY = + state.nextNextItemTranslationY( + revealHeight = revealHeight, + topItem = topItem, + nextNextItemHeight = placeable.height, + ) + scaleX = NextItemMinScale + scaleY = NextItemMinScale + } + } + } + } + } + } +} + +/** + * Masks the items following the current item (items that are below the current item on Z-axis) if + * the item's decoration fills the viewport width. + */ +private fun Modifier.maskItemsBelow(state: StackState, itemScope: StackItemScopeImpl): Modifier = + this.drawWithContent { + val viewportSize = state.layoutInfoInternal.viewportSize + val viewportWidth = viewportSize.width.toFloat() + + if (itemScope.maskWidth >= (viewportWidth - DecorationWidthNoiseThresholdPx)) { + // Only apply the mask to the items below (Z-axis) if the mask fills the viewport width. + val viewportHeight = viewportSize.height.toFloat() + drawRect( + Color.Black, + blendMode = BlendMode.DstOut, + // The coordinate space here is for the item's layout, which changes position in the + // stack viewport depending on the scroll position. We need to deduct the viewport + // height to make the starting Y offset negative, so that the mask region extends + // upwards to the top of the stack viewport. + topLeft = Offset(x = 0f, y = itemScope.maskBottomY - viewportHeight), + size = Size(width = viewportWidth, height = viewportHeight), + ) + } + + drawContent() + } + +/** + * The translation Y of the top item in pixels, which is always equal to the snapped position + * offset. + */ +private fun StackState.topItemTranslationY(revealHeight: Int, topItemHeight: Int) = + calculateTopPositionOffset(topItemHeight, revealHeight) + +/** The current translation Y of the next item in pixels. */ +private fun StackState.nextItemTranslationY( + revealHeight: Int, + topItem: Int, + nextItemHeight: Int, +): Float { + val nextPageOffset = pagerState.pageOffset(topItem + 1) + val progress = topItemOffsetFraction + val topItemHeight = layoutInfoInternal.measuredTopItemHeight + + val topPageTopPositionOffset = calculateTopPositionOffset(topItemHeight, revealHeight) + val nextPageTopPositionOffset = calculateTopPositionOffset(nextItemHeight, revealHeight) + + val topItemBottom = topPageTopPositionOffset + topItemHeight + // startOffset is the initial offset of the next item when it's revealed below the top item. + val startOffset = + calculateInitialBehindPosition( + itemAboveBottom = topItemBottom + revealHeight, + itemBehindHeight = nextItemHeight, + ) + + val currentOffset = + lerp(start = startOffset, stop = nextPageTopPositionOffset, fraction = progress) + + return currentOffset - nextPageOffset +} + +/** The current scale of the next item. */ +private fun StackState.nextItemScale(): Float { + val progress = topItemOffsetFraction + return lerp(start = NextItemMinScale, stop = 1f, fraction = progress) +} + +/** The current translation Y of the next-next item in pixels. */ +private fun StackState.nextNextItemTranslationY( + revealHeight: Int, + topItem: Int, + nextNextItemHeight: Int, +): Float { + val nextNextPageOffset = pagerState.pageOffset(topItem + 2) + val nextItemHeight = layoutInfoInternal.measuredNextItemHeight + + // Calculate where the next item *would* be if it were currently the top item. + val nextItemTopPositionOffset = calculateTopPositionOffset(nextItemHeight, revealHeight) + val nextItemTopPositionBottom = nextItemTopPositionOffset + nextItemHeight + + // Calculate the static target position the next-next item. + // We add revealHeight to nextItemBottom to ensure the item waits at the exact position + // where it will need to be when it transitions to being the next item. + val offset = + calculateInitialBehindPosition( + itemAboveBottom = nextItemTopPositionBottom + revealHeight, + itemBehindHeight = nextNextItemHeight, + ) + + return offset - nextNextPageOffset +} + +/** + * Calculates the offset from the top of the viewport for an item of a given height for the item's + * top snapped position. Items are aligned to the bottom of the stack layout. + */ +private fun StackState.calculateTopPositionOffset(itemHeight: Int, revealHeight: Int): Float { + val viewportHeight = layoutInfoInternal.viewportSize.height + return (viewportHeight - itemHeight - revealHeight).coerceAtLeast(0).toFloat() +} + +/** Calculates the initial offset for an item when it is positioned behind the item above it. */ +private fun calculateInitialBehindPosition(itemAboveBottom: Float, itemBehindHeight: Int): Float = + itemAboveBottom - itemBehindHeight * NextItemPositioningScale + +/** + * The main axis offset of the item in pixels from the top of the viewport. + * + * If the page is in the visible list (inside the viewport), we return its exact offset. If it's not + * in the list but is composed (due to beyondViewportPageCount), we calculate where it *should* be + * based on the current page and offset fraction. + * + * Note: this method assumes that the Pager's layoutInfo is already available. + */ +private fun PagerState.pageOffset(page: Int): Int { + val layoutInfo = layoutInfo + + // First check if the page is already visible. + val visiblePage = layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == page } + if (visiblePage != null) return visiblePage.offset + + // Calculate the distance in "pages" from the current snap position. + // (page - currentPage) gives the index distance. + // (- currentPageOffsetFraction) accounts for the sub-page scroll. + val offsetFromCurrentPage = page - currentPage - currentPageOffsetFraction + val stride = layoutInfo.pageSize + layoutInfo.pageSpacing + return (offsetFromCurrentPage * stride).roundToInt() +} + +/** The size of the area where the items beneath the top of the stack item are revealed. */ +internal val RevealAreaSize = 18.dp + +/** The maximum number of items that can be visible at the same time in addition to the top item. */ +private const val MaxNextVisibleItemCount = 2 + +/** + * The scale of the next item when it is fully behind the top item, and the default scale of the + * item behind it (the next-next item). + */ +private const val NextItemMinScale = 0.94f + +/** The scale factor that's between 1.0 and [NextItemMinScale], which is used in positioning. */ +private const val NextItemPositioningScale = 0.97f // (1f + NextItemMinScale) / 2f + +/** The animation spec used for snapping stack items. */ +private val SnapAnimationSpec = + spring( + dampingRatio = 0.56f, + stiffness = 118f, + visibilityThreshold = Int.VisibilityThreshold.toFloat(), + ) +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md new file mode 100644 index 0000000..c4acef7 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md @@ -0,0 +1,440 @@ +When creating a Glimmer Stack component, refer to the following source code in +`StackItemScope.kt` for setting the stack item scope: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.stack + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.PathMeasure +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.LayoutAwareModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.util.fastForEach +import androidx.xr.glimmer.DepthEffectNode +import androidx.xr.glimmer.GlimmerTheme.Companion.LocalGlimmerTheme +import kotlin.math.abs + +/** Receiver scope used by item content in [VerticalStack]. */ +@Stable +public sealed interface StackItemScope { + + /** + * Adds a decoration shape for this item, which is used in graphical effects applied to stack + * items, e.g., item masking and depth effects. For each distinct shape inside a stack item, + * this modifier should be applied to match that shape's bounds. For simple items with just one + * shape (such as a card), only one modifier is needed. + * + * Applying this modifier is optional but highly recommended. If not applied, the item will + * render normally but will not have the expected depth effect or masking behavior. Omitting + * this modifier is a valid use case when no visual item decoration effects are desired. + * + * @sample androidx.xr.glimmer.samples.VerticalStackSample + * @sample androidx.xr.glimmer.samples.VerticalStackWithMultipleShapesSample + * @param shape The shape of the element this modifier is applied to. + */ + public fun Modifier.itemDecoration(shape: Shape): Modifier = + this then ItemDecorationElement(this@StackItemScope as StackItemScopeImpl, shape) +} + +internal class ItemDecorationElement +internal constructor(private val stackItemScope: StackItemScopeImpl, private val shape: Shape) : + ModifierNodeElement() { + + override fun create(): ItemDecorationNode = ItemDecorationNode(stackItemScope, shape) + + override fun update(node: ItemDecorationNode) { + node.update(stackItemScope, shape) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ItemDecorationElement) return false + + if (stackItemScope != other.stackItemScope) return false + if (shape != other.shape) return false + + return true + } + + override fun hashCode(): Int { + var result = stackItemScope.hashCode() + result = 31 * result + shape.hashCode() + return result + } + + override fun InspectorInfo.inspectableProperties() { + name = "stackItemDecoration" + properties["shape"] = shape + } +} + +internal class StackItemScopeImpl(internal val state: StackState) : StackItemScope { + + internal var index = -1 + internal val decorations = mutableListOf() + internal var coordinates: LayoutCoordinates? = null + + internal var maskWidth by mutableFloatStateOf(0f) + private set + + internal var maskBottomY by mutableFloatStateOf(0f) + private set + + internal fun recalculateMask() { + // First find the max width to avoid unnecessary calls to calculateMaskBottomYInItem. + var maxDecorationWidth = 0f + decorations.fastForEach { decoration -> + val width = decoration.outline?.bounds?.width ?: 0f + if (width > maxDecorationWidth) { + maxDecorationWidth = width + } + } + + if (maxDecorationWidth <= 0f) { + maskWidth = 0f + maskBottomY = 0f + return + } + + var selectedDecoration: ItemDecorationNode? = null + var selectedDecorationMaskBottomY = 0f + + decorations.fastForEach { decoration -> + val outline = decoration.outline ?: return@fastForEach + val bounds = outline.bounds + + // Skip decorations that are narrower than the max found. + if (abs(bounds.width - maxDecorationWidth) > DecorationWidthNoiseThresholdPx) { + return@fastForEach + } + + val decorationMaskBottomY = decoration.calculateMaskBottomYInItem() + if (selectedDecoration == null) { + selectedDecoration = decoration + selectedDecorationMaskBottomY = decorationMaskBottomY + } else { + // Tie-breaking: prefer higher widest point (smaller Y). + if (decorationMaskBottomY < selectedDecorationMaskBottomY) { + selectedDecoration = decoration + selectedDecorationMaskBottomY = decorationMaskBottomY + } + } + } + + maskWidth = selectedDecoration?.outline?.bounds?.width ?: 0f + maskBottomY = selectedDecorationMaskBottomY + } +} + +internal class ItemDecorationNode( + private var stackItemScope: StackItemScopeImpl, + private var shape: Shape, +) : + DelegatingNode(), + LayoutAwareModifierNode, + CompositionLocalConsumerModifierNode, + DrawModifierNode { + + internal var size = Size.Zero + internal var outline: Outline? = null + internal var offset: Offset = Offset.Zero + + private var depthEffectNode: DepthEffectNode? = null + private var coordinates: LayoutCoordinates? = null + + private var lastOutlineForCachedMaskBottomY: Outline? = null + private var cachedMaskBottomY: Float = 0f + + private var minX = FloatArray(0) + private var maxX = FloatArray(0) + private var pathMeasure: PathMeasure? = null + + override fun onAttach() { + depthEffectNode = delegate(DepthEffectNode(currentValueOfDepth(), shape)) + stackItemScope.decorations.add(this) + if (size != Size.Zero) { + // If this node is reused, update the decoration in case there is no remeasure. + updateDecoration() + } + } + + override fun onRemeasured(size: IntSize) { + this.size = size.toSize() + updateDecoration() + } + + override fun onPlaced(coordinates: LayoutCoordinates) { + this.coordinates = coordinates + offset = calculateDecorationOffset() + stackItemScope.recalculateMask() + } + + override fun onDetach() { + stackItemScope.decorations.remove(this) + depthEffectNode?.let { undelegate(it) } + } + + override fun ContentDrawScope.draw() { + val index = stackItemScope.index + if (index == -1) return + val state = stackItemScope.state + val topItem = state.topItem + val offsetFraction = state.topItemOffsetFraction + + val contentAlpha = + calculateContentAlpha(index = index, topItem = topItem, offsetFraction = offsetFraction) + + // Apply alpha to the depth separately from the content so that the shadows are not clipped. + depthEffectNode?.apply { drawDepthEffect(alpha = contentAlpha) } + + // Draw item content with a scrim based on the current alpha. + drawContent() + + val scrimAlpha = + calculateScrimAlpha(index = index, topItem = topItem, offsetFraction = offsetFraction) + val scrimColor = getScrimColor(index = index, topItem = topItem) + val outline = getDecorationOutline() + scrimColor?.let { + // If there is a scrim color, apply it on top of the item. + drawOutline(outline = outline, color = it, alpha = scrimAlpha) + } + + if (contentAlpha < 1f) { + drawOutline( + outline = outline, + blendMode = BlendMode.DstOut, + color = Color.Black, + alpha = 1f - contentAlpha, + ) + } + } + + fun update(stackItemScope: StackItemScopeImpl, shape: Shape) { + depthEffectNode?.update(currentValueOfDepth(), shape) + if (this.stackItemScope != stackItemScope || this.shape != shape) { + this.stackItemScope.decorations.remove(this) + stackItemScope.decorations.add(this) + this.stackItemScope = stackItemScope + this.shape = shape + updateDecoration() + } + } + + internal fun getOrCalculateMaskBottomY(): Float { + if (outline !== lastOutlineForCachedMaskBottomY) { + cachedMaskBottomY = outline?.calculateMaskBottomY() ?: 0f + lastOutlineForCachedMaskBottomY = outline + } + return cachedMaskBottomY + } + + private fun ContentDrawScope.getDecorationOutline(): Outline { + outline?.let { + return it + } + return shape.createOutline(size, layoutDirection, this).also { outline = it } + } + + private fun updateDecoration() { + if (size == Size.Zero || !isAttached) return + + offset = calculateDecorationOffset() + outline = createOutline() + + stackItemScope.recalculateMask() + } + + private fun createOutline(): Outline { + val density = currentValueOf(LocalDensity) + val layoutDirection = currentValueOf(LocalLayoutDirection) + return shape.createOutline(size, layoutDirection, density) + } + + private fun calculateDecorationOffset(): Offset = + coordinates?.let { decorationCoordinates -> + stackItemScope.coordinates?.localPositionOf( + sourceCoordinates = decorationCoordinates, + relativeToSource = Offset.Zero, + ) + } ?: Offset.Zero + + private fun calculateContentAlpha(index: Int, topItem: Int, offsetFraction: Float): Float = + when { + index.isTopItem(topItem = topItem) -> 1f - offsetFraction + index.isNextItem(topItem = topItem) -> 1f + index.isNextNextItem(topItem = topItem) -> offsetFraction + else -> 0f + } + + private fun calculateScrimAlpha(index: Int, topItem: Int, offsetFraction: Float): Float = + when { + index.isNextItem(topItem = topItem) -> (1f - offsetFraction) * MaxItemScrimAlpha + index.isNextNextItem(topItem = topItem) -> MaxItemScrimAlpha + else -> 0f + } + + private fun getScrimColor(index: Int, topItem: Int): Color? = + when { + index.isNextItem(topItem = topItem) -> SurfaceLow + index.isNextNextItem(topItem = topItem) -> SurfaceLow + else -> null + } + + private fun currentValueOfDepth() = currentValueOf(LocalGlimmerTheme).depthEffectLevels.level1 + + /** + * Returns the Y coordinate within [Outline] where the bottom boundary of the mask should be. + */ + private fun Outline.calculateMaskBottomY(): Float = + when (this) { + is Outline.Rectangle -> 0f + is Outline.Rounded -> + // The mask's bottom is where the top corners of the round rect start curving. + roundRect.top + + maxOf(roundRect.topLeftCornerRadius.y, roundRect.topRightCornerRadius.y) + is Outline.Generic -> calculateYAtWidestPoint(this) + } + + /** + * Returns the Y coordinate within [Outline] where the outline is the widest. + * + * This uses a heuristic approach by sampling points along the path perimeter using + * [PathMeasure] and aggregating them into vertical buckets to determine the min/max X for a + * given Y. Shapes with sharp horizontal features might have their width slightly under-reported + * depending on sampling phase. + */ + private fun calculateYAtWidestPoint(outline: Outline.Generic): Float { + val bounds = outline.bounds + val height = bounds.height + if (height <= 0f) return 0f + + // 1 bucket every 2 pixels clamped to a reasonable range. + val resolution = + (height / 2) + .toInt() + .coerceIn( + minimumValue = MinWidestPointSearchResolution, + maximumValue = MaxWidestPointSearchResolution, + ) + + if (minX.size < resolution || maxX.size < resolution) { + // Allocate for max resolution to avoid reallocating the arrays on size changes. + minX = FloatArray(MaxWidestPointSearchResolution) + maxX = FloatArray(MaxWidestPointSearchResolution) + } + + minX.fill(Float.POSITIVE_INFINITY, fromIndex = 0, toIndex = resolution) + maxX.fill(Float.NEGATIVE_INFINITY, fromIndex = 0, toIndex = resolution) + + val measure = pathMeasure ?: PathMeasure().also { pathMeasure = it } + measure.setPath(outline.path, forceClosed = true) + val length = measure.length + if (length == 0f) return 0f + + // 2 samples per bucket. + val totalSamples = resolution * 2 + val stepSize = length / totalSamples + + // Scale for converting Y to a bucket index. + val indexScale = (resolution - 1) / height + val boundsTop = bounds.top + + var distance = 0f + while (distance <= length) { + val position = measure.getPosition(distance) + val x = position.x + val y = position.y + + val relativeY = y - boundsTop + val bucketIndex = (relativeY * indexScale).toInt().coerceIn(0, resolution - 1) + + if (x < minX[bucketIndex]) minX[bucketIndex] = x + if (x > maxX[bucketIndex]) maxX[bucketIndex] = x + + distance += stepSize + } + + // Default to the vertical center if no points are processed or path is empty. + var selectedIndex = resolution / 2 + var maxFoundWidth = -1f + for (i in 0 until resolution) { + if (minX[i] == Float.POSITIVE_INFINITY || maxX[i] == Float.NEGATIVE_INFINITY) continue + val width = maxX[i] - minX[i] + // If widths are effectively equal, we preserve the lower index. + if (width > maxFoundWidth + DecorationWidthNoiseThresholdPx) { + maxFoundWidth = width + selectedIndex = i + } + } + + return boundsTop + (height * selectedIndex / resolution.toFloat()) + } +} + +/** + * Calculates the Y coordinate within the stack item where the bottom boundary of the mask should + * be. + */ +private fun ItemDecorationNode.calculateMaskBottomYInItem(): Float = + offset.y + getOrCalculateMaskBottomY() + +/** Returns whether the index is of the top of the stack item. */ +internal fun Int.isTopItem(topItem: Int) = this == topItem + +/** Returns whether the index is of the next item that follows the top of the stack item. */ +internal fun Int.isNextItem(topItem: Int) = this == topItem + 1 + +/** Returns whether the index is of the next-next item after the top of the stack item. */ +internal fun Int.isNextNextItem(topItem: Int) = this == topItem + 2 + +/** + * Tolerance in pixels used to account for floating-point and heuristic estimation inaccuracies when + * comparing decoration widths. + */ +internal const val DecorationWidthNoiseThresholdPx = 0.5f + +private val SurfaceLow = Color(0xFF4F4F4F) +private const val MaxItemScrimAlpha = 0.5f +private const val MinWidestPointSearchResolution = 20 +private const val MaxWidestPointSearchResolution = 200 +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md new file mode 100644 index 0000000..1a9930c --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md @@ -0,0 +1,325 @@ +When creating a Glimmer Stack component, refer to the following source code in +`StackState.kt` for creating a state for the stack: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.stack + +import androidx.annotation.IntRange +import androidx.collection.MutableIntIntMap +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.spring +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.annotation.FrequentlyChangingValue +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.unit.IntSize + +/** + * Creates and remembers a [StackState] for a [VerticalStack]. + * + * The returned [StackState] is remembered across compositions and can be used to control or observe + * the state of a [VerticalStack]. It's essential to pass this state to the `state` parameter of the + * corresponding [VerticalStack] composable. + * + * Note: Properties of the state will only be correctly populated after the [VerticalStack] it is + * associated with has been composed for the first time. + * + * Warning: A single [StackState] instance must not be shared across multiple [VerticalStack] + * composables. + * + * @param initialTopItem The index of the item to show at the top of the stack initially. Must be + * non-negative. Defaults to 0. + * @see StackState + * @see VerticalStack + */ +@Composable +public fun rememberStackState(@IntRange(from = 0) initialTopItem: Int = 0): StackState = + rememberSaveable(saver = StackState.Saver) { StackState(initialTopItem) } + +/** + * The [VerticalStack] state that allows programmatic control and observation of the stack's state. + * + * A [StackState] object can be created and remembered using [rememberStackState]. + * + * Note: Properties of the state will only be correctly populated after the [VerticalStack] it is + * associated with has been composed for the first time. + * + * Warning: A single [StackState] instance must not be shared across multiple [VerticalStack] + * composables. + * + * @param initialTopItem The index of the item to show at the top of the stack initially. Must be + * non-negative. Defaults to 0. + * @see rememberStackState + * @see VerticalStack + */ +// TODO(b/413429531): add ScrollIndicatorState. +@Stable +public class StackState(@IntRange(from = 0) initialTopItem: Int = 0) : ScrollableState { + + init { + require(initialTopItem >= 0) { "initialTopItem must be non-negative" } + } + + internal var itemCount by mutableIntStateOf(0) + + internal val pagerState = PagerState(currentPage = initialTopItem, pageCount = { itemCount }) + + /** The index of the item that's currently at the top of the stack, defaults to 0. */ + public val topItem: Int + get() = topItemState.value + + /** + * Backing state for [topItem] derived from [PagerState.currentPage] and + * [PagerState.currentPageOffsetFraction]. + * + * In Stack, an item is considered the top of the stack item until it completely moves off the + * viewport (when scrolling forward), or until the previous item enters the viewport (when + * scrolling backward). + */ + internal val topItemState = derivedStateOf { + if (pagerState.currentPageOffsetFraction >= 0) pagerState.currentPage + else pagerState.currentPage - 1 + } + + /** + * The offset of the top item as a fraction of the stack item container size. The value + * indicates how much the item is offset from the snapped position. This value ranges between + * 0.0 (snapped position) and 1.0 (lower bound of the top item is at the top of the viewport). + */ + public val topItemOffsetFraction: Float + @FrequentlyChangingValue + get() { + // In Pager, [PagerState.currentPage] changes to the next page when the current page + // scrolls more than half way off the viewport, which is also when + // [PagerState.currentPageOffsetFraction] reaches 0.5. Similarly, when scrolling back, + // the [PagerState.currentPage] switches to the previous page when + // [PagerState.currentPageOffsetFraction] reaches -0.5. In other words, the current + // page's offset fraction ranges between -0.5 and 0.5. In Stack, an item is considered + // the top of the stack item until it completely moves off the viewport when scrolling + // forward, or until the previous item enters the viewport when scrolling backward. In + // other words, the top item's offset fraction ranges between 0 (at the snapped + // position) to 1.0 (at the top of the viewport). + val currentPageOffsetFraction = pagerState.currentPageOffsetFraction + return if (currentPageOffsetFraction >= 0) currentPageOffsetFraction + else currentPageOffsetFraction + 1f + } + + /** + * [InteractionSource] that's used to dispatch drag events when this stack is being dragged. To + * know whether a fling (or animated scroll) is in progress, use [isScrollInProgress]. + */ + public val interactionSource: InteractionSource + get() = pagerState.interactionSource + + /** + * Contains useful information about the currently displayed layout of this stack. The + * information is available after the first measure pass. + */ + // TODO(b/446933128): when making layoutInfo public, consider making it a State. + internal val layoutInfoInternal = StackLayoutInfoImpl(pagerState, topItemState) + + private var hasFocus: Boolean = false + private var focusedItem: Int = initialTopItem + + /** + * Scroll (jump immediately) to a given [item] index. + * + * @param item The index of the destination item + */ + public suspend fun scrollToItem(item: Int) { + if (itemCount == 0) return + pagerState.scrollToPage(item.coerceIn(0, itemCount - 1)) + } + + /** + * Scroll animate to a given [item]'s closest snap position. If the [item] is too far away from + * [topItem], not all the items in the range will be composed. Instead, the stack will jump to a + * nearer item, then compose and animate the rest of the items until the destination [item]. + * + * @param item The index of the destination item + * @param animationSpec An [AnimationSpec] to move between items + */ + public suspend fun animateScrollToItem( + item: Int, + animationSpec: AnimationSpec = spring(), + ) { + if (itemCount == 0) return + pagerState.animateScrollToPage( + item.coerceIn(0, itemCount - 1), + pageOffsetFraction = 0f, + animationSpec, + ) + } + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + if (itemCount == 0) return + pagerState.scroll(scrollPriority, block) + } + + override fun dispatchRawDelta(delta: Float): Float { + if (itemCount == 0) return 0f + return pagerState.dispatchRawDelta(delta) + } + + override val isScrollInProgress: Boolean + get() = pagerState.isScrollInProgress + + @get:Suppress("GetterSetterNames") + override val canScrollForward: Boolean + get() = pagerState.currentPage < pagerState.pageCount - 1 + + @get:Suppress("GetterSetterNames") + override val canScrollBackward: Boolean + get() = pagerState.currentPage > 0 + + @get:Suppress("GetterSetterNames") + override val lastScrolledForward: Boolean + get() = pagerState.lastScrolledForward + + @get:Suppress("GetterSetterNames") + override val lastScrolledBackward: Boolean + get() = pagerState.lastScrolledBackward + + /** Callback for top-level (pager-level) focus state changes. */ + internal fun onTopLevelFocusChanged(focusState: FocusState) { + hasFocus = focusState.hasFocus + } + + /** Callback for item-level focus state changes for the item at [index]. */ + internal fun onItemFocusChanged(index: Int, focusState: FocusState) { + if (focusState.isFocused) focusedItem = index + } + + /** + * Moves focus to [index] either to the current top item or the next item depending on whether + * the top item has moved past [FocusMoveThreshold] and if the item is not already in focus. + * + * If the stack doesn't already have focus, the auto focus logic doesn't apply. + */ + internal fun notifyAutoFocus(index: Int, focusRequester: FocusRequester) { + if (!hasFocus) { + // Do not move focus if the stack doesn't already have focus. + return + } + + val topItemValue = topItem + val intendedFocusedItem = + if (topItemOffsetFraction < FocusMoveThreshold) topItemValue + else (topItemValue + 1).coerceAtMost(itemCount - 1) + + if (intendedFocusedItem != index) { + // The intended focused item is not the item at the requested index. + return + } + + if (intendedFocusedItem == focusedItem) { + // No need to move focus if the intended focused item is already in focus. + return + } + + focusRequester.requestFocus() + } + + public companion object { + /** The default [Saver] implementation for [StackState]. */ + public val Saver: Saver = + Saver(save = { it.topItem }, restore = { StackState(it) }) + } +} + +/** + * Contains useful information about the currently displayed layout of a [VerticalStack]. This + * information is available after the first measure pass. + * + * Use [StackState.layoutInfoInternal] to retrieve this. + */ +@Stable +internal sealed interface StackLayoutInfo { + // TODO(b/446933128): decide what properties should be exposed as public States. +} + +/** The default implementation of [StackLayoutInfo]. */ +internal class StackLayoutInfoImpl +internal constructor(private val pagerState: PagerState, private val topItemState: State) : + StackLayoutInfo { + + /** The overall size of this stack's viewport. */ + internal val viewportSize: IntSize + get() = pagerState.layoutInfo.viewportSize + + /** The measured height of the top of the stack item. */ + internal val measuredTopItemHeight: Int + get() = measuredHeights.getOrDefault(topItemState.value, defaultValue = 0) + + /** The measured height of the item following the top of the stack item. */ + internal val measuredNextItemHeight: Int + get() = measuredHeights.getOrDefault(topItemState.value + 1, defaultValue = 0) + + /** The measured height of the item following the next item in the stack. */ + internal val measuredNextNextItemHeight: Int + get() = measuredHeights.getOrDefault(topItemState.value + 2, defaultValue = 0) + + /** The backing storage for measured item heights keyed by item index. */ + // TODO(b/446933128): remove this once PageInfo exposes page sizes. + private val measuredHeights: MutableIntIntMap = MutableIntIntMap() + + /** + * Updates the measured height of the item at the specified index and trims heights for items + * outside of the close range to the top item. + */ + internal fun updateMeasuredHeight(index: Int, height: Int) { + measuredHeights.put(index, height) + + // Clean up measured heights for items that are not in the close range to the top item. + // TODO(b/446933128): find a way to access currentPage inside of withoutReadObservation. + val currentPage = pagerState.currentPage + Snapshot.withoutReadObservation { + val itemCount = pagerState.pageCount + val itemRange = currentPage - 2..(currentPage + 3).coerceAtMost(itemCount - 1) + measuredHeights.removeIf { index, _ -> index !in itemRange } + } + } +} + +/** + * The threshold of [StackState.topItemOffsetFraction] past which focus should automatically move to + * the next item. + */ +private const val FocusMoveThreshold = 0.6f +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md new file mode 100644 index 0000000..998d192 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md @@ -0,0 +1,761 @@ +When using surfaces to build a custom component, refer to the following source +code in `Surface.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.layout.layout +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.node.requireGraphicsContext +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.intellij.lang.annotations.Language + +/** + * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area + * or 'physical' boundary for components such as buttons and cards. A surface is responsible for: + * 1) Clipping: a surface clips its children to the shape specified by [shape] + * 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. + * 3) Background: a surface has a background color of [color]. + * 4) Depth effect: a surface can have different [DepthEffect] shadows for different states, as + * specified by [depthEffect]. + * 5) Content color: a surface provides a [contentColor] for text and icons inside the surface. By + * default this is calculated from the provided background color. + * 6) Interaction states: when focused, a surface displays draws a wider border with a focused + * highlight on top. When pressed, a surface draws a pressed overlay. This happens for + * interactions emitted from [interactionSource], whether this surface is [focusable] or not. + * + * This surface is focusable by default - set [focusable] to false for un-interactive / decorative + * surfaces. For handling clicks, use the other [surface] overload with an `onClick` parameter. + * + * Simple usage: + * + * @sample androidx.xr.glimmer.samples.SurfaceSample + * + * For custom gesture handling, add the gesture modifier after this [surface], and provide a shared + * [MutableInteractionSource] to enable this surface to handle focus / press states. You should also + * pass `false` for [focusable] if that modifier already includes a focus target by default. For + * example, to create a toggleable surface: + * + * @sample androidx.xr.glimmer.samples.ToggleableSurfaceSample + * @param focusable whether this surface is focusable, true by default. Most surfaces should be + * focusable to allow navigation between surfaces in a screen. Unfocusable surfaces may be used + * for decorative only elements, such as surfaces used in a compound component with a separate + * focusable area. + * @param shape the [Shape] used to clip this surface, and also used to draw the background and + * border + * @param color the background [Color] for this surface + * @param contentColor the [Color] for content inside this surface + * @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect] + * shadows rendered in different states. + * @param border an optional inner border for this surface + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting [Interaction]s for this surface. Note that if `null` is provided, interactions will + * still happen internally. + */ +@Composable +public fun Modifier.surface( + focusable: Boolean = true, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + depthEffect: SurfaceDepthEffect? = null, + border: BorderStroke? = SurfaceDefaults.border(), + interactionSource: MutableInteractionSource? = null, +): Modifier { + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + return this.surfaceDepthEffect(depthEffect, shape, interactionSource) + .clip(shape) + .contentColorProvider(contentColor) + .then(SurfaceNodeElement(shape, border, interactionSource)) + .background(color = color, shape = shape) + .focusable(enabled = focusable, interactionSource = interactionSource) +} + +/** + * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area + * or 'physical' boundary for components such as buttons and cards. A surface is responsible for: + * 1) Clipping: a surface clips its children to the shape specified by [shape] + * 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. When + * focused, a surface draws a wider border with a focused highlight on top to indicate the focus + * state. + * 3) Background: a surface has a background color of [color]. + * 4) Depth effect: a surface can have different [DepthEffect] shadows for different states, as + * specified by [depthEffect]. + * 5) Content color: a surface provides a [contentColor] for text and icons inside the surface. By + * default this is calculated from the provided background color. + * 6) Interaction states: when focused, a surface displays draws a wider border with a focused + * highlight on top. When pressed, a surface draws a pressed overlay. This happens for + * interactions emitted from [interactionSource], whether this surface is [enabled] or not. + * + * This surface is focusable and handles clicks. For non-clickable surfaces, use the other overload + * of [surface] instead. For surfaces with custom gesture handling, refer to the sample and guidance + * on the other overload of [surface]. + * + * @sample androidx.xr.glimmer.samples.ClickableSurfaceSample + * @param enabled whether this surface is enabled, true by default. When false, this surface will + * not respond to user input, and will not be focusable. + * @param shape the [Shape] used to clip this surface, and also used to draw the background and + * border + * @param color the background [Color] for this surface + * @param contentColor the [Color] for content inside this surface + * @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect] + * shadows rendered in different states. + * @param border an optional inner border for this surface + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting [Interaction]s for this surface. Note that if `null` is provided, interactions will + * still happen internally. + * @param onClick callback invoked when this surface is clicked + */ +@Composable +public fun Modifier.surface( + enabled: Boolean = true, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + depthEffect: SurfaceDepthEffect? = null, + border: BorderStroke? = SurfaceDefaults.border(), + interactionSource: MutableInteractionSource? = null, + onClick: () -> Unit, +): Modifier { + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + return this.surfaceDepthEffect(depthEffect, shape, interactionSource) + .clip(shape) + .contentColorProvider(contentColor) + .then(SurfaceNodeElement(shape, border, interactionSource)) + .background(color = color, shape = shape) + // TODO: b/423573184 align on disabled behavior / state + .clickable(enabled = enabled, interactionSource = interactionSource, onClick = onClick) +} + +/** + * Represents the [DepthEffect] used by a [surface] in different states. + * + * Focused [surface]s with a [focusedDepthEffect] will have a higher zIndex set so they can draw + * their focused depth effect over siblings. + * + * @property [depthEffect] the [DepthEffect] used when the [surface] is in its default state (no + * other interactions are ongoing) + * @property [focusedDepthEffect] the [DepthEffect] used when the [surface] is focused + */ +@Immutable +public class SurfaceDepthEffect( + public val depthEffect: DepthEffect?, + public val focusedDepthEffect: DepthEffect?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SurfaceDepthEffect) return false + + if (depthEffect != other.depthEffect) return false + if (focusedDepthEffect != other.focusedDepthEffect) return false + + return true + } + + override fun hashCode(): Int { + var result = depthEffect?.hashCode() ?: 0 + result = 31 * result + (focusedDepthEffect?.hashCode() ?: 0) + return result + } +} + +/** Default values used for [surface]. */ +public object SurfaceDefaults { + /** + * Create the default [BorderStroke] used for a [surface]. Use the other overload in order to + * change the width or color. + */ + @Composable + public fun border(): BorderStroke { + return GlimmerTheme.LocalGlimmerTheme.current.defaultSurfaceBorder + } + + /** + * Create the default [BorderStroke] used for a [surface], with optional overrides for [width] + * and [color]. + * + * @param width width of the border in [Dp]. Use [Dp.Hairline] for one-pixel border. + * @param color color to paint the border with + */ + @Composable + public fun border( + width: Dp = DefaultSurfaceBorderWidth, + color: Color = GlimmerTheme.colors.outline, + ): BorderStroke { + return BorderStroke(width, color) + } + + /** Returns the default (cached) border for a [surface]. */ + internal val GlimmerTheme.defaultSurfaceBorder: BorderStroke + get() { + return defaultSurfaceBorderCached + ?: BorderStroke(DefaultSurfaceBorderWidth, colors.outline).also { + defaultSurfaceBorderCached = it + } + } +} + +/** + * Surface node responsible for drawing the border, focused border and highlight, and pressed + * overlay. + */ +private class SurfaceNodeElement( + private val shape: Shape, + private val border: BorderStroke?, + private val interactionSource: InteractionSource?, +) : ModifierNodeElement() { + override fun create(): SurfaceNode = SurfaceNode(shape, border, interactionSource) + + override fun update(node: SurfaceNode) = node.update(shape, border, interactionSource) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SurfaceNodeElement) return false + + if (shape != other.shape) return false + if (border != other.border) return false + if (interactionSource != other.interactionSource) return false + + return true + } + + override fun hashCode(): Int { + var result = shape.hashCode() + result = 31 * result + (border?.hashCode() ?: 0) + result = 31 * result + (interactionSource?.hashCode() ?: 0) + return result + } + + override fun InspectorInfo.inspectableProperties() { + name = "surface" + properties["shape"] = shape + properties["border"] = border + properties["interactionSource"] = interactionSource + } +} + +private class SurfaceNode( + private var shape: Shape, + private var border: BorderStroke?, + private var interactionSource: InteractionSource?, +) : DrawModifierNode, Modifier.Node() { + + override val shouldAutoInvalidate = false + + // Cache borders and highlight for unfocused and focused states. This means we + // can avoid recreating these for a given surface, if the border and shape never + // change. Changing between unfocused and focus states only requires a draw + // invalidation, as the borders are already cached. + + // Unfocused border + private val unfocusedBorderLogic: BorderLogic = BorderLogic() + private val unfocusedBorderWidth: () -> Dp = { border?.width ?: 0.dp } + // Focused border - this consists of two layers. A 'base' layer (which is the + // unfocused border with a different size) and the highlight we draw on top of + // this base layer. We need to increase the size of the underlying border to + // make sure that the highlight area fully matches the underlying border, to + // avoid inconsistent areas of coverage due to the transparency of the + // highlight. + private var focusedBorderLogic: BorderLogic? = null + private var focusedHighlightBorderLogic: BorderLogic? = null + private var focusedBorderWidth: (() -> Dp)? = null + + // Highlight shader / brush + var shader: Shader? = null + var shaderBrush: Brush? = null + + // Graphics Layer + private var unfocusedBorderLayer: GraphicsLayer? = null + private var unfocusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null + + private var focusedBorderLayer: GraphicsLayer? = null + private var focusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null + + private var focusedHighlightBorderLayer: GraphicsLayer? = null + private var focusedHighlightGraphicsLayerProvider: (() -> GraphicsLayer)? = null + + private var interactionCollectionJob: Job? = null + + // Enter / exit animation progress for the width and fade effect applied to the highlight + private var _focusedHighlightProgress: Animatable? = null + private val focusedHighlightProgress + get() = _focusedHighlightProgress?.value ?: 0f + + // Rotation progress applied to the highlight + private var _focusedHighlightRotationProgress: Animatable? = null + private val focusedHighlightRotationProgress + get() = _focusedHighlightRotationProgress?.value ?: 0f + + private var pressedOverlayAlpha: Animatable? = null + // Job that runs for a minimum duration to make sure quick presses are still visible + private var minimumPressDuration: Job? = null + private var pressReleaseAnimation: Job? = null + + fun update(shape: Shape, border: BorderStroke?, interactionSource: InteractionSource?) { + if (this.shape != shape) { + this.shape = shape + invalidateDraw() + } + if (this.border != border) { + this.border = border + invalidateDraw() + } + if (this.interactionSource != interactionSource) { + this.interactionSource = interactionSource + observeInteractions() + } + } + + override fun onAttach() { + observeInteractions() + } + + var isFocused = false + set(value) { + if (field != value) { + field = value + if (value) { + startFocusAnimation() + } else { + stopFocusAnimation() + } + // No need to invalidate the border cache - we build it ahead of time to account for + // focus changes. Just invalidate draw so we can switch to drawing the correct + // border. + invalidateDraw() + } + } + + var isPressed = false + set(value) { + if (field != value) { + field = value + if (value) { + pressedOverlayAlpha = pressedOverlayAlpha ?: Animatable(0f) + pressReleaseAnimation?.cancel() + minimumPressDuration?.cancel() + + minimumPressDuration = + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + delay(PressedOverlayMinimumDurationMillis) + } + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + pressedOverlayAlpha?.animateTo( + PressedOverlayAlpha, + PressedOverlayEnterAnimationSpec, + ) + } + } else { + pressedOverlayAlpha?.let { progress -> + pressReleaseAnimation = + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + minimumPressDuration?.join() + progress.animateTo(0f, PressedOverlayExitAnimationSpec) + } + } + } + invalidateDraw() + } + } + + private fun observeInteractions() { + interactionCollectionJob?.cancel() + interactionCollectionJob = null + isFocused = false + isPressed = false + interactionSource?.let { source -> + interactionCollectionJob = + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + var focusCount = 0 + var pressCount = 0 + source.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> focusCount++ + is FocusInteraction.Unfocus -> focusCount-- + is PressInteraction.Press -> pressCount++ + is PressInteraction.Release -> pressCount-- + is PressInteraction.Cancel -> pressCount-- + } + isFocused = focusCount > 0 + isPressed = pressCount > 0 + } + } + } + } + + private fun startFocusAnimation() { + _focusedHighlightProgress = _focusedHighlightProgress ?: Animatable(0f) + _focusedHighlightRotationProgress = _focusedHighlightRotationProgress ?: Animatable(0f) + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + _focusedHighlightProgress?.snapTo(0f) + _focusedHighlightProgress?.animateTo( + targetValue = 1f, + animationSpec = FocusedEnterAnimationSpec, + ) + } + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + _focusedHighlightRotationProgress?.snapTo(0f) + _focusedHighlightRotationProgress?.animateTo( + targetValue = 1f, + animationSpec = FocusedHighlightRotationAnimationSpec, + ) + } + } + + private fun stopFocusAnimation() { + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + _focusedHighlightProgress?.animateTo( + targetValue = 0f, + animationSpec = FocusedExitAnimationSpec, + ) + if (isActive) { + _focusedHighlightRotationProgress?.snapTo(0f) + } + } + } + + override fun ContentDrawScope.draw() { + val outline = shape.createOutline(size, layoutDirection, this) + drawContent() + val pressedOverlayColor = PressedOverlayColor.copy(alpha = pressedOverlayAlpha?.value ?: 0f) + drawRect(color = pressedOverlayColor) + if (border != null) { + val progress = focusedHighlightProgress + val gContext = requireGraphicsContext() + if (progress > 0f) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val rotationProgressRadians = + (FocusedHighlightRotationEndAngleRadians - + FocusedHighlightRotationStartAngleRadians) * + focusedHighlightRotationProgress + val rotationRadians = + FocusedHighlightRotationStartAngleRadians + rotationProgressRadians + shader = + HighlightShaderHelper.configureShader( + shader = shader, + size = size, + rotationRadians = rotationRadians.toFloat(), + progress = progress, + ) + shaderBrush = shaderBrush ?: ShaderBrush(shader!!) + } + focusedBorderLogic = focusedBorderLogic ?: BorderLogic() + focusedHighlightBorderLogic = focusedHighlightBorderLogic ?: BorderLogic() + focusedBorderWidth = + focusedBorderWidth + ?: { + val b = border + if (b != null) { + lerp( + b.width, + FocusedSurfaceBorderWidth, + // Capture class property instead of function-local progress to + // make sure this will read the animation state when the lambda + // is invoked and not capture a stale variable + focusedHighlightProgress, + ) + } else { + 0.dp + } + } + focusedBorderLogic!!.drawBorder( + this, + focusedBorderWidth!!, + border!!.brush, + focusedGraphicsLayerProvider + ?: { + focusedBorderLayer + ?: gContext.createGraphicsLayer().also { + focusedBorderLayer = it + } + } + .also { focusedGraphicsLayerProvider = it }, + outline, + ) + + shaderBrush?.let { + focusedHighlightBorderLogic!!.drawBorder( + this, + focusedBorderWidth!!, + it, + focusedHighlightGraphicsLayerProvider + ?: { + focusedHighlightBorderLayer + ?: gContext.createGraphicsLayer().also { + focusedHighlightBorderLayer = it + } + } + .also { focusedHighlightGraphicsLayerProvider = it }, + outline, + ) + } + } else { + unfocusedBorderLogic.drawBorder( + this, + unfocusedBorderWidth, + border!!.brush, + unfocusedGraphicsLayerProvider + ?: { + unfocusedBorderLayer + ?: gContext.createGraphicsLayer().also { + unfocusedBorderLayer = it + } + } + .also { unfocusedGraphicsLayerProvider = it }, + outline, + ) + } + } + } + + override fun onDetach() { + _focusedHighlightProgress = null + _focusedHighlightRotationProgress = null + pressedOverlayAlpha = null + + unfocusedGraphicsLayerProvider = null + val gContext = requireGraphicsContext() + unfocusedBorderLayer?.let { + gContext.releaseGraphicsLayer(it) + unfocusedBorderLayer = null + } + + focusedGraphicsLayerProvider = null + focusedBorderLayer?.let { + gContext.releaseGraphicsLayer(it) + focusedBorderLayer = null + } + + focusedHighlightGraphicsLayerProvider = null + focusedHighlightBorderLayer?.let { + gContext.releaseGraphicsLayer(it) + focusedHighlightBorderLayer = null + } + } +} + +/** + * Renders and animates a [surface]'s [depthEffect] for a given [shape], by observing + * [interactionSource]. + */ +@Composable +private fun Modifier.surfaceDepthEffect( + depthEffect: SurfaceDepthEffect?, + shape: Shape, + interactionSource: InteractionSource, +): Modifier { + if (depthEffect == null) return this + val focusedProgress = remember { Animatable(0f) } + // If focused and there is focused depth effect, we need to draw the surface on top of + // other siblings to make sure the depth effect occludes siblings. + val zIndex by remember { + // Derived to avoid invalidating layout each frame of the animation + derivedStateOf { + if (depthEffect.focusedDepthEffect != null && focusedProgress.value >= 0.5f) 1f else 0f + } + } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> + launch(start = CoroutineStart.UNDISPATCHED) { + focusedProgress.animateTo(1f, FocusedEnterAnimationSpec) + } + is FocusInteraction.Unfocus -> + launch(start = CoroutineStart.UNDISPATCHED) { + focusedProgress.animateTo(0f, FocusedExitAnimationSpec) + } + } + } + } + + return layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { placeable.place(0, 0, zIndex = zIndex) } + } + .depthEffect( + from = depthEffect.depthEffect, + to = depthEffect.focusedDepthEffect, + shape = shape, + progress = { focusedProgress.value }, + ) +} + +/** Default border width for a [surface]. */ +private val DefaultSurfaceBorderWidth = 2.dp + +/** Focused border width for a [surface]. */ +private val FocusedSurfaceBorderWidth = 5.dp + +/** Enter animation for focus highlight and depth effect */ +private val FocusedEnterAnimationSpec: AnimationSpec = + spring(dampingRatio = 1f, stiffness = 600f) + +/** Exit animation for focus highlight and depth effect */ +private val FocusedExitAnimationSpec: AnimationSpec = + spring(dampingRatio = 1f, stiffness = 100f) + +private val FocusedHighlightRotationAnimationSpec: AnimationSpec = + tween(durationMillis = 700, easing = LinearOutSlowInEasing, delayMillis = 40) + +private val FocusedHighlightRotationStartAngleRadians: Double = Math.toRadians(-100.0) + +private val FocusedHighlightRotationEndAngleRadians: Double = Math.toRadians(35.0) + +private val PressedOverlayColor = Color.White + +private const val PressedOverlayAlpha = 0.16f + +private val PressedOverlayEnterAnimationSpec: AnimationSpec = + spring(dampingRatio = 0.84f, stiffness = 8000f) + +private val PressedOverlayExitAnimationSpec: AnimationSpec = + spring(dampingRatio = 0.85f, stiffness = 50f) + +private const val PressedOverlayMinimumDurationMillis = 300L + +@Language(value = "AGSL") +private const val FocusedHighlightShader = + """ +/** + * Rotating linear gradient shader, where rotation is controlled by iRotation uniform. + * This is essentially the same as: + * LinearGradientShader( + * colors = FocusedHighlightColors, + * colorStops = FocusedHighlightColorStops, + * from = Offset.Zero, + * to = Offset(size.width, size.height), + * ) + * But allowing for efficient rotation, instead of needing to create a new shader / brush every + * frame with new coordinates. + */ +// Width / height +uniform float2 iResolution; +// Rotation in radians. 0 radians means a horizontal gradient. +// Positive values will have the effect of rotating the gradient clockwise. +uniform float iRotation; +// Alpha animation progress from 0 to 1. This will be applied to the color stops so that each +// color stop will fade in. +uniform float iAlphaProgress; + +half4 main(float2 fragCoord) { + // Horizontal gradient + half4 colors[4]; + colors[0] = half4(1.0, 1.0, 1.0, 1.0 * iAlphaProgress); // White with 100% alpha + colors[1] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha + colors[2] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha + colors[3] = half4(1.0, 1.0, 1.0, 0.8 * iAlphaProgress); // White with 80% alpha + + // Stops for the horizontal gradient + float stops[4]; + stops[0] = 0.0; + stops[1] = 0.3; + stops[2] = 0.66; + stops[3] = 1.0; + + // Normalize + half2 uv = fragCoord.xy / iResolution.xy; + + // Offset around a rotational center + half2 rotationCenter = half2(0.5, 0.5); + uv -= rotationCenter; + + // Rotate + // We rotate in the opposite direction as we are rotating the coordinate we sample the gradient + // from. To create the effect of a gradient 'moving' clockwise, we need to move the + // coordinate in the opposite direction (counter-clockwise). + float2x2 matrix = float2x2(cos(-iRotation),-sin(-iRotation),sin(-iRotation),cos(-iRotation)); + uv *= matrix; + + // Translate back into [0,1] space + uv += rotationCenter; + + // Blend through stops using the x coordinate, since we have a horizontal gradient + half4 color = mix(colors[0], colors[1], smoothstep(stops[0], stops[1], uv.x)); + color = mix(color, colors[2], smoothstep(stops[1], stops[2], uv.x)); + color = mix(color, colors[3], smoothstep(stops[2], stops[3], uv.x)); + + return color; +} +""" + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +private object HighlightShaderHelper { + @JvmStatic + fun configureShader( + shader: Shader?, + size: Size, + rotationRadians: Float, + progress: Float, + ): Shader { + val shader = shader as? RuntimeShader ?: RuntimeShader(FocusedHighlightShader) + shader.setFloatUniform("iResolution", size.width, size.height) + shader.setFloatUniform("iRotation", rotationRadians) + shader.setFloatUniform("iAlphaProgress", progress) + return shader + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md new file mode 100644 index 0000000..2f44dba --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md @@ -0,0 +1,101 @@ +When creating a Glimmer Title Chip component, refer to the following +implementation samples in `TitleChipSamples.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.glimmer.Card +import androidx.xr.glimmer.GlimmerTheme +import androidx.xr.glimmer.Icon +import androidx.xr.glimmer.Text +import androidx.xr.glimmer.TitleChip +import androidx.xr.glimmer.TitleChipDefaults +import androidx.xr.glimmer.list.VerticalList + +@Composable +fun TitleChipSampleUsage() { + VerticalList( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), + ) { + item { TitleChipSample() } + item { TitleChipWithLeadingIconSample() } + item { TitleChipWithCardSample() } + } +} + +@Sampled +@Composable +fun TitleChipSample() { + TitleChip { Text("Messages") } +} + +@Sampled +@Composable +fun TitleChipWithLeadingIconSample() { + TitleChip(leadingIcon = { Icon(FavoriteIcon, "Localized description") }) { Text("Messages") } +} + +@Sampled +@Composable +fun TitleChipWithCardSample() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TitleChip { Text("Title Chip") } + Spacer(Modifier.height(TitleChipDefaults.associatedContentSpacing)) + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Card Content") + } + } +} + +@Preview +@Composable +private fun TitleChipPreview() { + GlimmerTheme { TitleChipSample() } +} + +@Preview +@Composable +private fun TitleChipWithLeadingIconPreview() { + GlimmerTheme { TitleChipWithLeadingIconSample() } +} + +@Preview +@Composable +private fun TitleChipWithCardPreview() { + GlimmerTheme { TitleChipWithCardSample() } +} +``` + +
\ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md new file mode 100644 index 0000000..3639cd0 --- /dev/null +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md @@ -0,0 +1,136 @@ +When creating a Glimmer Title Chip component, refer to the following source code +in `TitleChip.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Title Chip is a component used to provide context for associated content, such as a [Card]. They + * are designed for brief information, typically a short title, name, or status. Title chips are not + * focusable, and not interactable. + * + * @sample androidx.xr.glimmer.samples.TitleChipSample + * + * Title chips may have a leading icon to provide more context: + * + * @sample androidx.xr.glimmer.samples.TitleChipWithLeadingIconSample + * + * To use a title chip with another component, place the title chip + * [TitleChipDefaults.associatedContentSpacing] above the other component. For example, to use a + * title chip with a card: + * + * @sample androidx.xr.glimmer.samples.TitleChipWithCardSample + * @param modifier the [Modifier] to be applied to this title chip + * @param leadingIcon optional leading icon to be placed before the [content]. This is typically an + * [Icon]. + * @param shape the [Shape] used to clip this title chip, and also used to draw the background and + * border + * @param color background color of this title chip + * @param contentColor content color used by components inside [content] and [leadingIcon]. + * @param border the border to draw around this title chip + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param content the main content, typically [Text], to display inside this title chip + */ +@Composable +public fun TitleChip( + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.large, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = TitleChipDefaults.contentPadding, + content: @Composable RowScope.() -> Unit, +) { + val iconSize = GlimmerTheme.iconSizes.small + val horizontalInnerContentPadding = GlimmerTheme.componentSpacingValues.extraSmall + + CompositionLocalProvider(LocalTextStyle provides GlimmerTheme.typography.caption) { + Row( + modifier + .surface( + focusable = false, + shape = shape, + color = color, + contentColor = contentColor, + depthEffect = null, + border = border, + ) + .defaultMinSize(minHeight = MinimumHeight) + .widthIn(max = MaximumWidth) + .padding(contentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (leadingIcon != null) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) + } + Spacer(Modifier.width(horizontalInnerContentPadding)) + content() + Spacer(Modifier.width(horizontalInnerContentPadding)) + } + } +} + +/** Default values used for [TitleChip]. */ +public object TitleChipDefaults { + /** Default content padding for a [TitleChip]. */ + public val contentPadding: PaddingValues + @Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.extraSmall) + + /** + * Default spacing between the bottom of a [TitleChip] and content associated with this title + * chip, such as a [Card]. For example this can be used with a + * [androidx.compose.foundation.layout.Spacer], or with [padding]. + * + * @sample androidx.xr.glimmer.samples.TitleChipWithCardSample + */ + public val associatedContentSpacing: Dp + @Composable get() = GlimmerTheme.componentSpacingValues.medium +} + +/** Default minimum height for a [TitleChip] */ +private val MinimumHeight = 44.dp + +/** Default maximum width for a [TitleChip] */ +private val MaximumWidth = 352.dp +``` + +
\ No newline at end of file From 640c9e3d0d335c186652f412731ddf831659661c Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:57:45 +0100 Subject: [PATCH 07/61] Update README (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Jose Alcérreca Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cbc22b6..84b6915 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ ## Android skills -Android skills are a dedicated repository of AI-optimised, modular instructions and resources, to -help LLMs better understand and execute specific patterns that follow the best practices and -guidance on Android development from [developer.android.com](https://developer.android.com). +**Android skills** are a dedicated repository of **AI-optimized, modular instructions** and +resources, to help LLMs better understand and execute specific patterns that follow the best +practices and guidance on Android development +from [developer.android.com](https://developer.android.com). Android skills follow the [open-standard agent skills](https://agentskills.io/home) - markdown -files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information -on specialized domains and workflows. +files (SKILL.md) that provide a technical specification of a task, and **ground LLMs** with +information on specialized domains and workflows. + +Our Android skill development focuses on **use cases and workflows where evaluations show LLMs +underperform**. +We aren't prioritizing well-established areas where LLMs are already proficient, such +as basic Jetpack Compose best practices. To learn more, read the official documentation: From 55f41f97a9716ca5e692cb0ca6f1bd78cfc4417e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 30 Apr 2026 12:26:56 +0200 Subject: [PATCH 08/61] Improves Android CLI instructions (#30) * Improves Android CLI instructions Clarify usage of Android CLI for skill installation. * Rename '--path' option to '--project' in README --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84b6915..55ef253 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,16 @@ To learn more, read the official documentation: ### Install Android skills -Use Android CLI to install Android skills to directories for all detected agents: +Use Android CLI to install a specific skill into the current directory: ``` -android skills add [--all] [--agent=] [--skill=] +android skills add --skill=r8-analyzer --project=. +``` + +Use Android CLI to install all Android skills to directories for all detected agents: + +``` +android skills add --all ``` If you don't have any existing agent directories and don't specify particular agents, the skills @@ -39,6 +45,7 @@ will be installed for Gemini and Antigravity at `~/.gemini/antigravity/skills`. be installed for all detected agents. - `--skill` - Specific skill that you want to install. If omitted (and `--all` isn't specified), only the `android-cli` skill will be installed. +- `--project` - Path to a project root in which to install the skills. ## Disclaimer @@ -59,4 +66,4 @@ details. ## Review our community guidelines This project follows -[Google's Open Source Community Guidelines](https://opensource.google/conduct/). \ No newline at end of file +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). From bd8d64a49a0691c7423fee3c60ec203b881db05b Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Wed, 6 May 2026 07:39:20 +0000 Subject: [PATCH 09/61] Updates skills (2026-05-06 07:39) --- android-cli/base/SKILL.md | 207 ++++++++++++++ camera/camera1-to-camerax/SKILL.md | 266 ++++++++++++++++++ ...setup-compose-dependencies-and-compiler.md | 12 +- .../navigation-3/recipes/results-event.md | 4 +- .../navigation-3/recipes/results-state.md | 4 +- .../ai-glasses/first-activity.md | 30 +- .../references/button-source.md | 11 +- .../references/card-source.md | 90 +++--- .../references/iconbutton-source.md | 12 +- .../references/listitem-source.md | 30 +- .../references/surface-source.md | 132 +++------ .../references/titlechip-source.md | 1 - 12 files changed, 606 insertions(+), 193 deletions(-) create mode 100644 android-cli/base/SKILL.md create mode 100644 camera/camera1-to-camerax/SKILL.md diff --git a/android-cli/base/SKILL.md b/android-cli/base/SKILL.md new file mode 100644 index 0000000..07ff90e --- /dev/null +++ b/android-cli/base/SKILL.md @@ -0,0 +1,207 @@ +--- +name: base +description: Orchestrates Android development tasks +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - AndroidCLI +--- + +including project creation, deployment, SDK management, and environment +diagnostics using the `android` command-line tool. + +This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. + +## SDK management + +To manage the installation of Android SDKs and tools, use the `sdk` command. For example: + +- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` +- `android sdk update []`: Update a specific package or all packages to the latest version. +- `android sdk remove `: Remove a package from the local SDK. +- `android sdk list --all`: List installed and available SDK packages. + +## Project creation + +Create projects from templates using the `create` command. + +For example: `android create empty-activity --name="My App" --output=./my-app` + +## Interacting with devices + +For more information on interacting with running devices, see [here](https://developer.android.com/agents/skills/android-cli/base/references/interact) + +## Running journey tests + +For more information on running journeys, see [here](https://developer.android.com/agents/skills/android-cli/base/references/journeys) + +## Doc searching + +The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. +By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. +Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. + +Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: +- Finding migration guides for APIs. +- Finding examples for APIs. +- Finding up-to-date information about Android APIs. +- Finding best practices for Android concepts. + +## Running APKs + +Use the `run` command to run Android apps. + +## Managing emulators + +Manage Android Virtual Devices (AVDs) using the `android emulator` command + +## Capturing screenshots + +Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. + +## Managing skills + +Manage antigravity agent skills for Android using the `android skills` command. + +## Inspecting UI Layouts + +Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. + +## Updating the CLI + +Update the Android CLI using the `android update` command. + +# `android help` output + +Usage: android \[-hV\] \[--sdk=PARAM\] \[COMMAND\] +-h, --help Show this help message and exit. +--sdk=PARAM Path to the Android SDK +-V, --version Print version information and exit. +Commands: +create Create a new Android project +describe Analyzes an Android project to generate descriptive metadata. +docs Android documentation commands +emulator Emulator commands +help Shows the help of all commands +info Print environment information (SDK Location, etc.) +init Initializes the environment (eg. skills) for Android CLI. +layout Returns the layout tree of an application +run Deploy an Android Application +screen Commands to view the device +sdk Download and list SDK packages +skills Manage skills +update Update the Android CLI + +create +Usage: android create \[-h\] \[--verbose\] \[--list\] \[--minSdk=api\] +--name=applicationName \[-o=dest-path\] \[template-name\] +Create a new Android project +\[template-name\] The template name +-h, --help Show this help message and exit. +--minSdk=api The 'minSdk' supported by the application (default +is defined in the template) +--name=applicationName +The name of the application (e.g. 'My Application') +-o, --output=dest-path The destination project directory path (default is +'.') +--verbose Enables verbose output +--list List all available templates + +describe +Usage: android describe \[-hV\] \[--project_dir=PARAM\] +Analyzes an Android project to generate descriptive metadata. +This command identifies and outputs the paths to JSON files that detail the +project's structure, including build targets and their corresponding output +artifact locations (e.g., APKs). This information enables other tools and +commands to locate build artifacts efficiently. +-h, --help Show this help message and exit. +--project_dir=PARAM The project directory to describe +-V, --version Print version information and exit. + +docs +Usage: android docs \[-h\] \[COMMAND\] +Android documentation commands +-h, --help Show this help message and exit. +Commands: +search Search Android documentation +fetch Fetch Android documentation + +emulator +Usage: android emulator \[-h\] \[COMMAND\] +Emulator commands +-h, --help Show this help message and exit. +Commands: +create Creates a virtual device +start Launches the specified virtual device. This command will return when +the emulator is fully started and ready to use. +stop Stops the specified virtual device +list Lists available virtual devices +remove Delete a virtual device + +help +Usage: android help \[COMMAND\] +Shows the help of all commands +\[COMMAND\] The command to show help for + +info +Usage: android info Print environment information (SDK Location, etc.) The specific field to print the value of. If omitted print all. + +init +Usage: android init +Initializes the environment (eg. skills) for Android CLI. + +layout +Usage: android layout \[-dhp\] \[--device=PARAM\] \[-o=PARAM\] +Returns the layout tree of an application +-d, --diff Returns a flat list of the layout elements that have +changed since the last invocation of ui-dump +--device=PARAM The device serial number +-h, --help Show this help message and exit. +-o, --output=PARAM Writes the layout tree to the specified file or +directory. If omitted, prints the tree to standard +output +-p, --pretty Pretty-prints the returned JSON + +run +Usage: android run \[-h\] \[--debug\] \[--activity=PARAM\] \[--device=PARAM\] +\[--type=PARAM\] \[--apks=PARAM\[,PARAM...\]\]... +Deploy an Android Application +--activity=PARAM The activity name +--apks=PARAM\[,PARAM...\] +The paths to the APKs +--debug Run in debug mode +--device=PARAM The device serial number +-h, --help Show this help message and exit. +--type=PARAM The component type (ACTIVITY, SERVICE, etc.) + +screen +Usage: android screen \[-h\] \[COMMAND\] +Commands to view the device +-h, --help Show this help message and exit. +Commands: +capture Outputs the device screen to a PNG +resolve Target UI elements visually + +sdk +Usage: android sdk \[COMMAND\] +Download and list SDK packages +Commands: +install Install SDK packages +update Update one or all packages to the latest version +remove Remove a package from the SDK +list List installed and available SDK packages + +skills +Usage: android skills \[COMMAND\] +Manage skills +Commands: +add Install a skill +remove Remove a skill +list List available skills +find Find skills by keyword + +update +Usage: android update \[--url=PARAM\] +Update the Android CLI +--url=PARAM The URL to download the update from \ No newline at end of file diff --git a/camera/camera1-to-camerax/SKILL.md b/camera/camera1-to-camerax/SKILL.md new file mode 100644 index 0000000..52ce24a --- /dev/null +++ b/camera/camera1-to-camerax/SKILL.md @@ -0,0 +1,266 @@ +--- +name: camera1-to-camerax +description: Use this skill to migrate legacy Android camera implementations (Camera1 + or raw Camera2 APIs) to CameraX. CameraX is a lifecycle-aware Jetpack library built + on top of Camera2 that resolves camera rotation issues and handles device dependencies. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - Android + - CameraX + - Camera1 Migration + - Jetpack Compose + - Dependencies + - Image Capture + - Lifecycle + - PreviewView +--- + +## Step 0: Add Dependencies + +Check for and add the required CameraX dependencies. Use version 1.3.0 or higher +for interoperability, or version 1.5.0 or higher for Compose extensions. + +If you are using a Version Catalog (`libs.versions.toml`), add the following: + + [versions] + camerax = "" + + [libraries] + androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } + androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } + androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } + # If using Android Views (Option A) + androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } + # If using Jetpack Compose (Option B) + androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" } + +And in your `build.gradle.kts` (or `build.gradle`): + + // Core dependencies + implementation(libs.androidx.camera.core) + implementation(libs.androidx.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + + // If using Android Views (Option A) + implementation(libs.androidx.camera.view) + + // If using Jetpack Compose (Option B) + implementation(libs.androidx.camera.compose) + +Without a Version Catalog, fall back to these standard Gradle dependencies: + + // Core dependencies + implementation "androidx.camera:camera-core:" + implementation "androidx.camera:camera-camera2:" + implementation "androidx.camera:camera-lifecycle:" + + // If using Android Views (Option A) + implementation "androidx.camera:camera-view:" + + // If using Jetpack Compose (Option B) + implementation "androidx.camera:camera-compose:" + +## Step 1: Remove Legacy Implementation + +1. Delete all `android.hardware.Camera` instances. +2. Delete `SurfaceView` and `SurfaceHolder.Callback` implementations (`surfaceCreated`, `surfaceChanged`, `surfaceDestroyed`). +3. Remove custom lifecycle handling that opens or releases the camera in `onResume` or `onPause`. +4. Remove manual matrix calculations for orientation. + +## Step 2: Initialize ProcessCameraProvider + +Request the `ProcessCameraProvider` and bind use cases to the Activity or +Fragment lifecycle. + + +```kotlin +val context = LocalContext.current +val lifecycleOwner = LocalLifecycleOwner.current +LaunchedEffect(context, lifecycleOwner) { + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + + val preview = Preview.Builder().build() + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + cameraProvider.unbindAll() // Unbind before rebinding + + val camera = cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture + ) + val cameraControl = camera.cameraControl + }, ContextCompat.getMainExecutor(context) + ) +} +``` + +
+ +## Step 3: Implement the Preview \& Tap-to-Focus + +Choose exactly one of the following patterns based on the app's UI toolkit: + +### Option A: For Android Views (XML Legacy) + +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) +``` + +
+ +### Option B: For Jetpack Compose + +Use `androidx.camera.compose.CameraXViewfinder`. + +**1. Set up preview and SurfaceRequest**: + + +```kotlin +var surfaceRequest by remember { mutableStateOf(null) } +val preview = remember { + Preview.Builder().build().apply { + setSurfaceProvider { request -> surfaceRequest = request } + } +} +``` + +
+ +**2. Render viewfinder**: + + +```kotlin +surfaceRequest?.let { request -> + CameraXViewfinder( + surfaceRequest = request, + coordinateTransformer = coordinateTransformer, + modifier = Modifier + ) +} +``` + +
+ +**3. Handle tap-to-focus in Compose**: + + +```kotlin +// Inside your tap gesture handler... +val surfaceCoords = with(coordinateTransformer) { offset.transform() } +val factory = SurfaceOrientedMeteringPointFactory( + request.resolution.width.toFloat(), + request.resolution.height.toFloat() +) +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) { + if (!view.isInEditMode) { + val rotation = view.display?.rotation ?: Surface.ROTATION_0 + imageCapture.targetRotation = rotation + preview.targetRotation = rotation + } +} +``` + +
+ +## Step 4: Capture Photo + +Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles +rotation directly. + + +```kotlin +imageCapture.takePicture( + cameraExecutor, + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + + // Adjust rotation natively via ImageProxy + val matrix = Matrix() + matrix.postRotate(image.imageInfo.rotationDegrees.toFloat()) + if (lensFacing == CameraSelector.LENS_FACING_FRONT) { + matrix.postScale(-1f, 1f) // Mirror for front camera + } + + val rotatedBitmap = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true + ) + + // MUST close proxy + image.close() + } + + override fun onError(exception: ImageCaptureException) { + Log.e("CameraX", "Capture failed: ${exception.message}", exception) + } + } +) +``` + +
+ +## Step 5: Switch Cameras + +To flip between front and rear cameras, change the `CameraSelector` and +re-trigger the `ProcessCameraProvider` logic. + + +```kotlin +lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) { + CameraSelector.LENS_FACING_FRONT +} else { + CameraSelector.LENS_FACING_BACK +} +``` + +
+ +## What not to do. + +- **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`. +- **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations. +- **Don't forget to close the ImageProxy** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos. +- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an old fallback option that introduces resizing issues. \ 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 b661b5b..0771751 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 @@ -15,7 +15,7 @@ Set up the Compose Compiler Gradle plugin: 2. In the `versions` and `plugins` sections, add the new dependency: [versions] - kotlin = "2.3.10" + kotlin = "2.3.21" [plugins] org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -47,14 +47,14 @@ 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.10" // this version matches your Kotlin version + id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" // this version matches your Kotlin version } Add the classpath to your top-level project `build.gradle.kts` file: buildscript { dependencies { - classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.3.10") + classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.3.21") } } @@ -75,7 +75,7 @@ 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.03.00`. +Always use the latest Compose BOM version: `2026.04.01`. 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. @@ -104,7 +104,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - def composeBom = platform('androidx.compose:compose-bom:2026.03.00') + def composeBom = platform('androidx.compose:compose-bom:2026.04.01') implementation composeBom androidTestImplementation composeBom @@ -143,7 +143,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - val composeBom = platform("androidx.compose:compose-bom:2026.03.00") + val composeBom = platform("androidx.compose:compose-bom:2026.04.01") implementation(composeBom) androidTestImplementation(composeBom) 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 c105669..c8461fc 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 @@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable data object Home : NavKey @Serializable -class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt +class PersonDetailsForm : NavKey ``` ``` @@ -94,7 +94,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/Person.kt +data class Person(val name: String, val favoriteColor: String) : Parcelable ``` ``` 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 d2d349a..6a61883 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 @@ -68,7 +68,7 @@ import kotlinx.serialization.Serializable data object Home : NavKey @Serializable -class PersonDetailsForm : NavKeyhttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/NavKeys.kt +class PersonDetailsForm : NavKey ``` ``` @@ -94,7 +94,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelablehttps://github.com/android/nav3-recipes/blob/bcba753d0dca783eb8c7c5680a22d181cfd74eed/app/src/main/java/com/example/nav3recipes/results/common/Person.kt +data class Person(val name: String, val favoriteColor: String) : Parcelable ``` ``` diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md index 9c4760b..6e8767d 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md @@ -27,25 +27,23 @@ Add the following [library dependencies for AI glasses](https://developer.androi ### Groovy -```groovy -dependencies { - implementation "androidx.xr.runtime:runtime:1.0.0-alpha12" - implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha10" - implementation "androidx.xr.projected:projected:1.0.0-alpha06" - implementation "androidx.xr.arcore:arcore:1.0.0-alpha12" -} -``` + dependencies { + implementation "androidx.xr.runtime:runtime:1.0.0-alpha12" + implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha11" + implementation "androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11" + implementation "androidx.xr.projected:projected:1.0.0-alpha06" + implementation "androidx.xr.arcore:arcore:1.0.0-alpha12" + } ### Kotlin -```kotlin -dependencies { - implementation("androidx.xr.runtime:runtime:1.0.0-alpha12") - implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha10") - implementation("androidx.xr.projected:projected:1.0.0-alpha06") - implementation("androidx.xr.arcore:arcore:1.0.0-alpha12") -} -``` + dependencies { + implementation("androidx.xr.runtime:runtime:1.0.0-alpha12") + implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha11") + implementation("androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11") + implementation("androidx.xr.projected:projected:1.0.0-alpha06") + implementation("androidx.xr.arcore:arcore:1.0.0-alpha12") + } ## Declare your activity in your app's manifest diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md index 9c3caef..0d89012 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md @@ -22,6 +22,7 @@ When creating a Glimmer Button component, refer to the following source code in package androidx.xr.glimmer import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -33,6 +34,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -113,18 +115,23 @@ public fun Button( focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1, ) + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + CompositionLocalProvider(LocalTextStyle provides GlimmerTheme.typography.bodySmall) { Row( modifier .semantics { role = Role.Button } .surface( - enabled = enabled, shape = shape, color = color, contentColor = contentColor, depthEffect = depth, border = border, - interactionSource = interactionSource, + interactionSource = internalInteractionSource, + ) + .clickable( + enabled = enabled, + interactionSource = internalInteractionSource, onClick = onClick, ) .defaultMinSize(minHeight = minHeight) diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md index 5d87c22..7eb8d9b 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md @@ -22,6 +22,8 @@ When creating a Glimmer Card component, refer to the following source code in package androidx.xr.glimmer import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -34,6 +36,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -119,21 +122,24 @@ public fun Card( interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } CardImpl( - modifier = modifier, - onClick = null, - focusable = true, + modifier = + modifier + .surface( + shape = shape, + color = color, + contentColor = contentColor, + border = border, + interactionSource = internalInteractionSource, + ) + .focusable(interactionSource = internalInteractionSource), title = title, subtitle = subtitle, header = header, leadingIcon = leadingIcon, trailingIcon = trailingIcon, - shape = shape, - color = color, - contentColor = contentColor, - border = border, contentPadding = contentPadding, - interactionSource = interactionSource, content = content, ) } @@ -213,21 +219,24 @@ public fun Card( interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } CardImpl( - modifier = modifier, - onClick = onClick, - focusable = true, + modifier = + modifier + .surface( + shape = shape, + color = color, + contentColor = contentColor, + border = border, + interactionSource = internalInteractionSource, + ) + .clickable(interactionSource = internalInteractionSource, onClick = onClick), title = title, subtitle = subtitle, header = header, leadingIcon = leadingIcon, trailingIcon = trailingIcon, - shape = shape, - color = color, - contentColor = contentColor, - border = border, contentPadding = contentPadding, - interactionSource = interactionSource, content = content, ) } @@ -294,20 +303,19 @@ public fun Card( // b/436852852 - in a list the button won't be focused until it crosses the focus line. ActionCardLayout(modifier, action) { CardImpl( - modifier = Modifier, - onClick = null, - focusable = false, + modifier = + Modifier.surface( + shape = shape, + color = color, + contentColor = contentColor, + border = border, + ), title = title, subtitle = subtitle, header = header, leadingIcon = leadingIcon, trailingIcon = trailingIcon, - shape = shape, - color = color, - contentColor = contentColor, - border = border, contentPadding = contentPadding, - interactionSource = null, content = content, ) } @@ -316,19 +324,12 @@ public fun Card( @Composable private fun CardImpl( modifier: Modifier, - onClick: (() -> Unit)?, - focusable: Boolean, title: @Composable (() -> Unit)?, subtitle: @Composable (() -> Unit)?, header: @Composable (() -> Unit)?, leadingIcon: @Composable (() -> Unit)?, trailingIcon: @Composable (() -> Unit)?, - shape: Shape, - color: Color, - contentColor: Color, - border: BorderStroke?, contentPadding: PaddingValues, - interactionSource: MutableInteractionSource?, content: @Composable () -> Unit, ) { val iconSize = GlimmerTheme.iconSizes.large @@ -336,32 +337,9 @@ private fun CardImpl( val componentSpacingValues = GlimmerTheme.componentSpacingValues val innerPadding = componentSpacingValues.small val iconSpacing = componentSpacingValues.medium - val surfaceModifier = - if (onClick != null) { - Modifier.surface( - onClick = onClick, - shape = shape, - color = color, - contentColor = contentColor, - border = border, - interactionSource = interactionSource, - ) - } else { - Modifier.surface( - focusable = focusable, - shape = shape, - color = color, - contentColor = contentColor, - border = border, - interactionSource = interactionSource, - ) - } + Column( - modifier = - modifier - .then(surfaceModifier) - .defaultMinSize(minHeight = MinimumHeight) - .padding(contentPadding), + modifier = modifier.defaultMinSize(minHeight = MinimumHeight).padding(contentPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md index 35e522f..f7e5d6d 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md @@ -22,6 +22,7 @@ When creating a Glimmer Icon component, refer to the following source code in package androidx.xr.glimmer import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -82,17 +84,23 @@ public fun IconButton( depthEffect = null, focusedDepthEffect = GlimmerTheme.depthEffectLevels.level1, ) + + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + Box( modifier .semantics { role = Role.Button } .surface( - enabled = enabled, shape = shape, color = color, contentColor = contentColor, depthEffect = depthEffect, border = border, - interactionSource = interactionSource, + interactionSource = internalInteractionSource, + ) + .clickable( + enabled = enabled, + interactionSource = internalInteractionSource, onClick = onClick, ) .defaultMinSize(IconButtonDefaults.minimumSize) diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md index e1dba94..d414811 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md @@ -22,6 +22,8 @@ code in `ListItem.kt` for list items: package androidx.xr.glimmer import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,6 +34,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -208,27 +211,28 @@ private fun ListItemImpl( focusedDepthEffect = GlimmerTheme.depthEffectLevels.level4, ) + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + val surfaceModifier = - if (onClick != null) { - Modifier.surface( - onClick = onClick, + Modifier.surface( shape = shape, color = color, contentColor = contentColor, depthEffect = depthEffect, border = border, - interactionSource = interactionSource, + interactionSource = internalInteractionSource, ) - } else { - Modifier.surface( - shape = shape, - color = color, - contentColor = contentColor, - depthEffect = depthEffect, - border = border, - interactionSource = interactionSource, + .then( + if (onClick != null) { + Modifier.clickable( + interactionSource = internalInteractionSource, + onClick = onClick, + ) + } else { + Modifier.focusable(interactionSource = internalInteractionSource) + } ) - } + Row( modifier = modifier diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md index 998d192..5f0f057 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md @@ -32,12 +32,9 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -71,70 +68,11 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.intellij.lang.annotations.Language +// TODO: b/423573184 align on disabled behavior / state /** * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area - * or 'physical' boundary for components such as buttons and cards. A surface is responsible for: - * 1) Clipping: a surface clips its children to the shape specified by [shape] - * 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. - * 3) Background: a surface has a background color of [color]. - * 4) Depth effect: a surface can have different [DepthEffect] shadows for different states, as - * specified by [depthEffect]. - * 5) Content color: a surface provides a [contentColor] for text and icons inside the surface. By - * default this is calculated from the provided background color. - * 6) Interaction states: when focused, a surface displays draws a wider border with a focused - * highlight on top. When pressed, a surface draws a pressed overlay. This happens for - * interactions emitted from [interactionSource], whether this surface is [focusable] or not. - * - * This surface is focusable by default - set [focusable] to false for un-interactive / decorative - * surfaces. For handling clicks, use the other [surface] overload with an `onClick` parameter. - * - * Simple usage: - * - * @sample androidx.xr.glimmer.samples.SurfaceSample - * - * For custom gesture handling, add the gesture modifier after this [surface], and provide a shared - * [MutableInteractionSource] to enable this surface to handle focus / press states. You should also - * pass `false` for [focusable] if that modifier already includes a focus target by default. For - * example, to create a toggleable surface: - * - * @sample androidx.xr.glimmer.samples.ToggleableSurfaceSample - * @param focusable whether this surface is focusable, true by default. Most surfaces should be - * focusable to allow navigation between surfaces in a screen. Unfocusable surfaces may be used - * for decorative only elements, such as surfaces used in a compound component with a separate - * focusable area. - * @param shape the [Shape] used to clip this surface, and also used to draw the background and - * border - * @param color the background [Color] for this surface - * @param contentColor the [Color] for content inside this surface - * @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect] - * shadows rendered in different states. - * @param border an optional inner border for this surface - * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and - * emitting [Interaction]s for this surface. Note that if `null` is provided, interactions will - * still happen internally. - */ -@Composable -public fun Modifier.surface( - focusable: Boolean = true, - shape: Shape = GlimmerTheme.shapes.medium, - color: Color = GlimmerTheme.colors.surface, - contentColor: Color = calculateContentColor(color), - depthEffect: SurfaceDepthEffect? = null, - border: BorderStroke? = SurfaceDefaults.border(), - interactionSource: MutableInteractionSource? = null, -): Modifier { - val interactionSource = interactionSource ?: remember { MutableInteractionSource() } - return this.surfaceDepthEffect(depthEffect, shape, interactionSource) - .clip(shape) - .contentColorProvider(contentColor) - .then(SurfaceNodeElement(shape, border, interactionSource)) - .background(color = color, shape = shape) - .focusable(enabled = focusable, interactionSource = interactionSource) -} - -/** - * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area - * or 'physical' boundary for components such as buttons and cards. A surface is responsible for: + * or 'physical' boundary for components such as buttons and cards. A [surface] implements shared + * visual decoration for Jetpack Compose Glimmer components: * 1) Clipping: a surface clips its children to the shape specified by [shape] * 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. When * focused, a surface draws a wider border with a focused highlight on top to indicate the focus @@ -146,15 +84,25 @@ public fun Modifier.surface( * default this is calculated from the provided background color. * 6) Interaction states: when focused, a surface displays draws a wider border with a focused * highlight on top. When pressed, a surface draws a pressed overlay. This happens for - * interactions emitted from [interactionSource], whether this surface is [enabled] or not. + * interactions emitted from [interactionSource]. * - * This surface is focusable and handles clicks. For non-clickable surfaces, use the other overload - * of [surface] instead. For surfaces with custom gesture handling, refer to the sample and guidance - * on the other overload of [surface]. + * Use surface on its own for decorative elements that cannot be interacted with by a user: + * + * @sample androidx.xr.glimmer.samples.SurfaceSample + * + * In most cases surfaces should be interactive, to allow users to consistently move focus and + * navigate between components. You can use [androidx.compose.foundation.focusable] for focus-only + * surfaces, or [androidx.compose.foundation.clickable] and other modifiers for surfaces with + * actions. To ensure the surface correctly reflects the interaction states, provide the same + * [InteractionSource] to all modifiers. + * + * For example, to create a clickable surface: * * @sample androidx.xr.glimmer.samples.ClickableSurfaceSample - * @param enabled whether this surface is enabled, true by default. When false, this surface will - * not respond to user input, and will not be focusable. + * + * Similarly, to create a focusable surface: + * + * @sample androidx.xr.glimmer.samples.FocusableSurfaceSample * @param shape the [Shape] used to clip this surface, and also used to draw the background and * border * @param color the background [Color] for this surface @@ -162,30 +110,25 @@ public fun Modifier.surface( * @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect] * shadows rendered in different states. * @param border an optional inner border for this surface - * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and - * emitting [Interaction]s for this surface. Note that if `null` is provided, interactions will - * still happen internally. - * @param onClick callback invoked when this surface is clicked + * @param interactionSource the [InteractionSource] that emits [Interaction]s for this surface. For + * interactive surfaces, the [InteractionSource] instance provided to this surface must be shared + * with the modifier responsible for emitting [Interaction]s, such as + * [androidx.compose.foundation.focusable] or [androidx.compose.foundation.clickable]. */ @Composable public fun Modifier.surface( - enabled: Boolean = true, shape: Shape = GlimmerTheme.shapes.medium, color: Color = GlimmerTheme.colors.surface, contentColor: Color = calculateContentColor(color), depthEffect: SurfaceDepthEffect? = null, border: BorderStroke? = SurfaceDefaults.border(), - interactionSource: MutableInteractionSource? = null, - onClick: () -> Unit, + interactionSource: InteractionSource? = null, ): Modifier { - val interactionSource = interactionSource ?: remember { MutableInteractionSource() } return this.surfaceDepthEffect(depthEffect, shape, interactionSource) .clip(shape) .contentColorProvider(contentColor) .then(SurfaceNodeElement(shape, border, interactionSource)) .background(color = color, shape = shape) - // TODO: b/423573184 align on disabled behavior / state - .clickable(enabled = enabled, interactionSource = interactionSource, onClick = onClick) } /** @@ -603,7 +546,7 @@ private class SurfaceNode( private fun Modifier.surfaceDepthEffect( depthEffect: SurfaceDepthEffect?, shape: Shape, - interactionSource: InteractionSource, + interactionSource: InteractionSource?, ): Modifier { if (depthEffect == null) return this val focusedProgress = remember { Animatable(0f) } @@ -615,17 +558,20 @@ private fun Modifier.surfaceDepthEffect( if (depthEffect.focusedDepthEffect != null && focusedProgress.value >= 0.5f) 1f else 0f } } - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { interaction -> - when (interaction) { - is FocusInteraction.Focus -> - launch(start = CoroutineStart.UNDISPATCHED) { - focusedProgress.animateTo(1f, FocusedEnterAnimationSpec) - } - is FocusInteraction.Unfocus -> - launch(start = CoroutineStart.UNDISPATCHED) { - focusedProgress.animateTo(0f, FocusedExitAnimationSpec) - } + if (interactionSource != null) { + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> + launch(start = CoroutineStart.UNDISPATCHED) { + focusedProgress.animateTo(1f, FocusedEnterAnimationSpec) + } + + is FocusInteraction.Unfocus -> + launch(start = CoroutineStart.UNDISPATCHED) { + focusedProgress.animateTo(0f, FocusedExitAnimationSpec) + } + } } } } diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md index 3639cd0..a876a81 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md @@ -86,7 +86,6 @@ public fun TitleChip( Row( modifier .surface( - focusable = false, shape = shape, color = color, contentColor = contentColor, From 8fa0eab759c00ec6f2d2eba673b4b171ab75cced Mon Sep 17 00:00:00 2001 From: Esteban de la Canal Date: Wed, 6 May 2026 16:41:56 -0700 Subject: [PATCH 10/61] Update android-cli skills --- android-cli/SKILL.md | 197 +++++++++++++++++++++++++++ android-cli/base/SKILL.md | 207 ----------------------------- android-cli/references/interact.md | 83 ++++++++++++ android-cli/references/journeys.md | 97 ++++++++++++++ 4 files changed, 377 insertions(+), 207 deletions(-) create mode 100644 android-cli/SKILL.md delete mode 100644 android-cli/base/SKILL.md create mode 100644 android-cli/references/interact.md create mode 100644 android-cli/references/journeys.md diff --git a/android-cli/SKILL.md b/android-cli/SKILL.md new file mode 100644 index 0000000..4a5b44b --- /dev/null +++ b/android-cli/SKILL.md @@ -0,0 +1,197 @@ +--- +name: android-cli +description: Orchestrates Android development tasks including project creation, deployment, SDK management, and environment diagnostics using the `android` command-line tool. +--- +# Android CLI Specialist + +This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. + +## SDK management +To manage the installation of Android SDKs and tools, use the `sdk` command. For example: + +- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` +- `android sdk update []`: Update a specific package or all packages to the latest version. +- `android sdk remove `: Remove a package from the local SDK. +- `android sdk list --all`: List installed and available SDK packages. + +## Project creation +Create projects from templates using the `create` command. + +For example: `android create empty-activity --name="My App" --output=./my-app` + +## Interacting with devices +For more information on interacting with running devices, see [here](references/interact.md) + +## Running journey tests +For more information on running journeys, see [here](references/journeys.md) + +## Doc searching +The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. +By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. +Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. + +Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: + - Finding migration guides for APIs. + - Finding examples for APIs. + - Finding up-to-date information about Android APIs. + - Finding best practices for Android concepts. + +## Running APKs +Use the `run` command to run Android apps. + +## Managing emulators + +Manage Android Virtual Devices (AVDs) using the `android emulator` command + +## Capturing screenshots + +Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. + +## Managing skills + +Manage antigravity agent skills for Android using the `android skills` command. + +## Inspecting UI Layouts + +Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. + +## Updating the CLI + +Update the Android CLI using the `android update` command. + +# `android help` output + +Usage: android [-hV] [--sdk=PARAM] [COMMAND] + -h, --help Show this help message and exit. + --sdk=PARAM Path to the Android SDK + -V, --version Print version information and exit. +Commands: + create Create a new Android project + describe Analyzes an Android project to generate descriptive metadata. + docs Android documentation commands + emulator Emulator commands + help Shows the help of all commands + info Print environment information (SDK Location, etc.) + init Initializes the environment (eg. skills) for Android CLI. + layout Returns the layout tree of an application + run Deploy an Android Application + screen Commands to view the device + sdk Download and list SDK packages + skills Manage skills + update Update the Android CLI + +create + Usage: android create [-h] [--verbose] [--list] [--minSdk=api] + --name=applicationName [-o=dest-path] [template-name] + Create a new Android project + [template-name] The template name + -h, --help Show this help message and exit. + --minSdk=api The 'minSdk' supported by the application (default + is defined in the template) + --name=applicationName + The name of the application (e.g. 'My Application') + -o, --output=dest-path The destination project directory path (default is + '.') + --verbose Enables verbose output + --list List all available templates + +describe + Usage: android describe [-hV] [--project_dir=PARAM] + Analyzes an Android project to generate descriptive metadata. + This command identifies and outputs the paths to JSON files that detail the + project's structure, including build targets and their corresponding output + artifact locations (e.g., APKs). This information enables other tools and + commands to locate build artifacts efficiently. + -h, --help Show this help message and exit. + --project_dir=PARAM The project directory to describe + -V, --version Print version information and exit. + +docs + Usage: android docs [-h] [COMMAND] + Android documentation commands + -h, --help Show this help message and exit. + Commands: + search Search Android documentation + fetch Fetch Android documentation + +emulator + Usage: android emulator [-h] [COMMAND] + Emulator commands + -h, --help Show this help message and exit. + Commands: + create Creates a virtual device + start Launches the specified virtual device. This command will return when + the emulator is fully started and ready to use. + stop Stops the specified virtual device + list Lists available virtual devices + remove Delete a virtual device + +help + Usage: android help [COMMAND] + Shows the help of all commands + [COMMAND] The command to show help for + +info + Usage: android info + Print environment information (SDK Location, etc.) + The specific field to print the value of. If omitted print all. + +init + Usage: android init + Initializes the environment (eg. skills) for Android CLI. + +layout + Usage: android layout [-dhp] [--device=PARAM] [-o=PARAM] + Returns the layout tree of an application + -d, --diff Returns a flat list of the layout elements that have + changed since the last invocation of ui-dump + --device=PARAM The device serial number + -h, --help Show this help message and exit. + -o, --output=PARAM Writes the layout tree to the specified file or + directory. If omitted, prints the tree to standard + output + -p, --pretty Pretty-prints the returned JSON + +run + Usage: android run [-h] [--debug] [--activity=PARAM] [--device=PARAM] + [--type=PARAM] [--apks=PARAM[,PARAM...]]... + Deploy an Android Application + --activity=PARAM The activity name + --apks=PARAM[,PARAM...] + The paths to the APKs + --debug Run in debug mode + --device=PARAM The device serial number + -h, --help Show this help message and exit. + --type=PARAM The component type (ACTIVITY, SERVICE, etc.) + +screen + Usage: android screen [-h] [COMMAND] + Commands to view the device + -h, --help Show this help message and exit. + Commands: + capture Outputs the device screen to a PNG + resolve Target UI elements visually + +sdk + Usage: android sdk [COMMAND] + Download and list SDK packages + Commands: + install Install SDK packages + update Update one or all packages to the latest version + remove Remove a package from the SDK + list List installed and available SDK packages + +skills + Usage: android skills [COMMAND] + Manage skills + Commands: + add Install a skill + remove Remove a skill + list List available skills + find Find skills by keyword + +update + Usage: android update [--url=PARAM] + Update the Android CLI + --url=PARAM The URL to download the update from + diff --git a/android-cli/base/SKILL.md b/android-cli/base/SKILL.md deleted file mode 100644 index 07ff90e..0000000 --- a/android-cli/base/SKILL.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -name: base -description: Orchestrates Android development tasks -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - keywords: - - AndroidCLI ---- - -including project creation, deployment, SDK management, and environment -diagnostics using the `android` command-line tool. - -This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. - -## SDK management - -To manage the installation of Android SDKs and tools, use the `sdk` command. For example: - -- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` -- `android sdk update []`: Update a specific package or all packages to the latest version. -- `android sdk remove `: Remove a package from the local SDK. -- `android sdk list --all`: List installed and available SDK packages. - -## Project creation - -Create projects from templates using the `create` command. - -For example: `android create empty-activity --name="My App" --output=./my-app` - -## Interacting with devices - -For more information on interacting with running devices, see [here](https://developer.android.com/agents/skills/android-cli/base/references/interact) - -## Running journey tests - -For more information on running journeys, see [here](https://developer.android.com/agents/skills/android-cli/base/references/journeys) - -## Doc searching - -The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. -By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. -Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. - -Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: -- Finding migration guides for APIs. -- Finding examples for APIs. -- Finding up-to-date information about Android APIs. -- Finding best practices for Android concepts. - -## Running APKs - -Use the `run` command to run Android apps. - -## Managing emulators - -Manage Android Virtual Devices (AVDs) using the `android emulator` command - -## Capturing screenshots - -Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. - -## Managing skills - -Manage antigravity agent skills for Android using the `android skills` command. - -## Inspecting UI Layouts - -Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. - -## Updating the CLI - -Update the Android CLI using the `android update` command. - -# `android help` output - -Usage: android \[-hV\] \[--sdk=PARAM\] \[COMMAND\] --h, --help Show this help message and exit. ---sdk=PARAM Path to the Android SDK --V, --version Print version information and exit. -Commands: -create Create a new Android project -describe Analyzes an Android project to generate descriptive metadata. -docs Android documentation commands -emulator Emulator commands -help Shows the help of all commands -info Print environment information (SDK Location, etc.) -init Initializes the environment (eg. skills) for Android CLI. -layout Returns the layout tree of an application -run Deploy an Android Application -screen Commands to view the device -sdk Download and list SDK packages -skills Manage skills -update Update the Android CLI - -create -Usage: android create \[-h\] \[--verbose\] \[--list\] \[--minSdk=api\] ---name=applicationName \[-o=dest-path\] \[template-name\] -Create a new Android project -\[template-name\] The template name --h, --help Show this help message and exit. ---minSdk=api The 'minSdk' supported by the application (default -is defined in the template) ---name=applicationName -The name of the application (e.g. 'My Application') --o, --output=dest-path The destination project directory path (default is -'.') ---verbose Enables verbose output ---list List all available templates - -describe -Usage: android describe \[-hV\] \[--project_dir=PARAM\] -Analyzes an Android project to generate descriptive metadata. -This command identifies and outputs the paths to JSON files that detail the -project's structure, including build targets and their corresponding output -artifact locations (e.g., APKs). This information enables other tools and -commands to locate build artifacts efficiently. --h, --help Show this help message and exit. ---project_dir=PARAM The project directory to describe --V, --version Print version information and exit. - -docs -Usage: android docs \[-h\] \[COMMAND\] -Android documentation commands --h, --help Show this help message and exit. -Commands: -search Search Android documentation -fetch Fetch Android documentation - -emulator -Usage: android emulator \[-h\] \[COMMAND\] -Emulator commands --h, --help Show this help message and exit. -Commands: -create Creates a virtual device -start Launches the specified virtual device. This command will return when -the emulator is fully started and ready to use. -stop Stops the specified virtual device -list Lists available virtual devices -remove Delete a virtual device - -help -Usage: android help \[COMMAND\] -Shows the help of all commands -\[COMMAND\] The command to show help for - -info -Usage: android info Print environment information (SDK Location, etc.) The specific field to print the value of. If omitted print all. - -init -Usage: android init -Initializes the environment (eg. skills) for Android CLI. - -layout -Usage: android layout \[-dhp\] \[--device=PARAM\] \[-o=PARAM\] -Returns the layout tree of an application --d, --diff Returns a flat list of the layout elements that have -changed since the last invocation of ui-dump ---device=PARAM The device serial number --h, --help Show this help message and exit. --o, --output=PARAM Writes the layout tree to the specified file or -directory. If omitted, prints the tree to standard -output --p, --pretty Pretty-prints the returned JSON - -run -Usage: android run \[-h\] \[--debug\] \[--activity=PARAM\] \[--device=PARAM\] -\[--type=PARAM\] \[--apks=PARAM\[,PARAM...\]\]... -Deploy an Android Application ---activity=PARAM The activity name ---apks=PARAM\[,PARAM...\] -The paths to the APKs ---debug Run in debug mode ---device=PARAM The device serial number --h, --help Show this help message and exit. ---type=PARAM The component type (ACTIVITY, SERVICE, etc.) - -screen -Usage: android screen \[-h\] \[COMMAND\] -Commands to view the device --h, --help Show this help message and exit. -Commands: -capture Outputs the device screen to a PNG -resolve Target UI elements visually - -sdk -Usage: android sdk \[COMMAND\] -Download and list SDK packages -Commands: -install Install SDK packages -update Update one or all packages to the latest version -remove Remove a package from the SDK -list List installed and available SDK packages - -skills -Usage: android skills \[COMMAND\] -Manage skills -Commands: -add Install a skill -remove Remove a skill -list List available skills -find Find skills by keyword - -update -Usage: android update \[--url=PARAM\] -Update the Android CLI ---url=PARAM The URL to download the update from \ No newline at end of file diff --git a/android-cli/references/interact.md b/android-cli/references/interact.md new file mode 100644 index 0000000..099e66b --- /dev/null +++ b/android-cli/references/interact.md @@ -0,0 +1,83 @@ +# Tools +Run `android layout --help` and `android screen --help`. + +## UI Dump +`android layout` returns a flat JSON list of the UI elements on screen. +`android layout --diff` returns a flat JSON list of the UI elements that have changed since the last call to `layout` or `layout --diff` + +Each JSON object represents a UI element in the Android app. The following properties may be present: +- `text` - any literal text the element contains +- `resourceId` - the Android resource id used to refer to the element +- `contentDesc` - a description of a UI element for use by accessibility tools +- `interactions` - the set of user interactions the element supports. May contain one or more of: `checkable`, `clickable`, `focusable`, `scrollable`, `long-clickable`, `password` +- `state` - the set of states the element is in. May contain one or more of `checked`, `focused`, `selected` +- `bounds` - the screen coordinates of the bounding rectangle of the element, in the format `[min X,min Y][max X, max Y]` +- `center` - the screen coordinates of the center of the element, in the format `[x,y]` +- `off-screen` - if true, the element is in the UI hierarchy but not visible; it may require scrolling to view. + +Use `layout` as a primary means of examining an Android app. Use `layout --diff` to focus on changes and to keep your context small. +Example: When entering digits into a calculator, use `layout --diff` to output only the digit readout element. + +`layout` may fail due to the app displaying a WebView or animation; in these cases, use `android screen --annotate` to inspect the app. +This failure will likely resolve after navigating away from the current screen. + +## Screenshot +`android screen capture -o ` saves a PNG of the current device screen to `` + +Use `screen capture` as a secondary means of examining an Android app +Examples: +- Understanding the content of an on-screen image +- Looking at a `WebView` (web content does not always appear in the ui dump) +- Trying to find a UI element by its visual appearance + +**IMPORTANT**: Always *VISUALLY* examine the PNG image returned from `android screen` BEFORE doing anything else. + +## Annotated Screenshot +`android screen capture --annotate -o ` +`android screen resolve --screen --string ` + +The `--annotate` command adds numerical labels and bounding boxes around UI elements. Use this command to locate UI elements that cannot +be located in the `layout` output. + +**IMPORTANT**: When using `android screen --annotate`, always *VISUALLY* examine the resulting PNG file. + +To refer to these labels in input commands, use `screen resolve` to convert labels into coordinates: + +`android screen resolve --screen --string "#3"` returns ` ` + +To save turns, you can combine shell commands: + +`adb shell input $(android screen resolve --screen screen.png --string "tap #34")` + +This command taps on region #34 from `screen.png` + +## Input +Use `adb shell input` for interacting with Android devices. +Refer to the `"interactions"` property of an element for what interactions can be performed on a particular element. + +Interact with UI elements with their `center` coordinate or their `bounds` coordinates: +```json +{ + "key": -248568265, + "class": "android.widget.Button", + "bounds": "[138,9][167,38]", + "center": "[152,23]" +} +``` +To tap on this button, you would execute `adb shell input tap 152 23`. This taps the center. + +```json +{ + "key": 12487234, + "class": "com.example.ui.ScrollableList", + "bounds": "[100,200][400,600]", + "center": "[250,400]" +} +``` +To scroll down on this list, you would execute `adb shell input swipe 250 400 600 500`. This swipes from the center to the bottom over 500ms. + +# Android Interaction Rules +1. Always ensure text input fields have `"focused"` in their `"state"` list before entering text +2. If an element has `"scrollable"` in its `"interactions"` list, try scrolling it when looking for missing UI elements +2. Always scroll slowly when executing scroll inputs. The 5th argument to `adb shell input swipe` controls scroll duration. +3. Content may take time to load; if a `layout` is missing information after you take an action, wait a few seconds, then perform `layout --diff` to see if anything changes. \ No newline at end of file diff --git a/android-cli/references/journeys.md b/android-cli/references/journeys.md new file mode 100644 index 0000000..7ed45ad --- /dev/null +++ b/android-cli/references/journeys.md @@ -0,0 +1,97 @@ +A journey is an XML-specified test of an Android app's behavior. It consists of a list of `` elements. For example: +```xml + + + A sample journey to illustrate the format + + + + Tap the "Home" icon + + + Verify that the app is on its Home screen + + + +``` + +Evaluate a journey by proceeding through the `` list in sequential order. Evaluate each `` block individually. +A journey succeeds if all elements in the `` list succeed. + +A journey is a test case for an app. The journey XML is the source of truth; if the app disagrees with the journey, the app has failed. +Additionally, if the app exits, crashes, or freezes, journey evaluation stops and the journey fails. + +**IMPORTANT** - Execute each step EXACTLY as written, and independently of other steps! If an action says to `"tap the first search result"`, +you MUST find the search results and tap the first one. Do this even if you believe you know the intent behind the action. + +## Taking Actions +Some `` elements specify UI interactions to perform on the running Android app. Perform the interaction and verify that the app does +not crash or behave in an unexpected manner. This is the *only* verification you should perform for an ``. + +If the interaction cannot be performed as specified, the journey fails. +Example: +```Click the red button``` +If you determine a red button is not present in the UI, the journey fails. + +If the text of an `` specifies a list of actions, break it into sub-actions and evaluate them individually: +Example: +```Search for soda and add the first result to the cart``` +This should be evaluated as: +``` +Search for soda +Add the first result to the cart +``` + +If an `` contains something that is not a specification for a UI interaction, alert the user that the journey is malformed and exit +early, specifying the error in question. + +## Verifying Expectations +`` elements that begin with "check" or "verify" specify expectations for the current state of the Android app. Determine the current +state of the app and check if the expectations are met. + +Determine the current state of the app by inspecting the current screen of the device without interacting with it. +Example: +```Check if "Switch 2" is visible on the screen``` +This requires only inspecting the current screen, not scrolling or interacting. If "Switch 2" is not currently visible, the action fails. + +If the expectations are not met, mark the `` as a failure and the journey evaluation ends. A single `` may contain +multiple expectations. +Example: +```Verify that the app is on the Home screen, the Home icon is blue, and the temperature is displayed``` +This `` fails if ANY of the following are false: +- The app is on the Home screen +- There is a Home icon, and it is blue +- A temperature is displayed + +## Handling failure +When running a journey, evaluate it as a test. Failure is acceptable, and often expected. Proper reporting of failures is the priority. + +Keep debugging and troubleshooting to a minimum; assume that tools are showing you the correct output every time. The goal is to determine +if the *current* Android app can correctly handle the *current* steps outlined in the journey. Suggestions for bug fixes, clarification, or +other improvements should be kept to journey evaluation summary at the end. + +## Summarizing +For each `` you evaluated, output JSON describing the results. + +``` +{ + "journey:", The name of the journey + "results:" [ + { + // A string containing the full text of the + "action": "Click the blue button, + // "PASSED" if the instruction was evaluated, "FAILED" if the instruction could not be evaluated, or "SKIPPED" if journey evaluation ended early because an instruction failed + "status": "PASSED", + // A list of the ADB commands executed while evaluating the instruction, + "commands": [ "adb input swipe 490 200 500 500 500", "adb input tap 45 920" ], + // Failure reasons, feedback, or other useful information + "comment": "The journey step doesn't specify that the button requires scrolling to see", + }, + { + "action": "The home screen is shown", + "status": "FAILED", + "comment": "The settings page was shown", + }, + ] +} +``` From e070ee2e3ec883aff11bcd0a384c50a12b4d5064 Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Thu, 7 May 2026 10:14:36 +1000 Subject: [PATCH 11/61] Update SKILL.md --- android-cli/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android-cli/SKILL.md b/android-cli/SKILL.md index 4a5b44b..da8b23b 100644 --- a/android-cli/SKILL.md +++ b/android-cli/SKILL.md @@ -1,6 +1,17 @@ --- name: android-cli description: Orchestrates Android development tasks including project creation, deployment, SDK management, and environment diagnostics using the `android` command-line tool. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - sdk + - emulator + - skills + - docs + - knowledge base + - project creation + - screenshots --- # Android CLI Specialist From 9a4905611c9f8f35ac47bb5e56b2d75e69afc75f Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Thu, 7 May 2026 01:19:05 +0000 Subject: [PATCH 12/61] Move to devtools folder --- {android-cli => devtools/android-cli}/SKILL.md | 0 {android-cli => devtools/android-cli}/references/interact.md | 0 {android-cli => devtools/android-cli}/references/journeys.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {android-cli => devtools/android-cli}/SKILL.md (100%) rename {android-cli => devtools/android-cli}/references/interact.md (100%) rename {android-cli => devtools/android-cli}/references/journeys.md (100%) diff --git a/android-cli/SKILL.md b/devtools/android-cli/SKILL.md similarity index 100% rename from android-cli/SKILL.md rename to devtools/android-cli/SKILL.md diff --git a/android-cli/references/interact.md b/devtools/android-cli/references/interact.md similarity index 100% rename from android-cli/references/interact.md rename to devtools/android-cli/references/interact.md diff --git a/android-cli/references/journeys.md b/devtools/android-cli/references/journeys.md similarity index 100% rename from android-cli/references/journeys.md rename to devtools/android-cli/references/journeys.md From 4b896c8c60342e17e268337e7d635cea389f4ab7 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 7 May 2026 15:25:49 +0000 Subject: [PATCH 13/61] Updates skills (2026-05-07 15:25) --- android-cli/base/SKILL.md | 207 +++++++++++++++++++ devtools/android-cli/SKILL.md | 208 -------------------- devtools/android-cli/references/interact.md | 83 -------- devtools/android-cli/references/journeys.md | 97 --------- 4 files changed, 207 insertions(+), 388 deletions(-) create mode 100644 android-cli/base/SKILL.md delete mode 100644 devtools/android-cli/SKILL.md delete mode 100644 devtools/android-cli/references/interact.md delete mode 100644 devtools/android-cli/references/journeys.md diff --git a/android-cli/base/SKILL.md b/android-cli/base/SKILL.md new file mode 100644 index 0000000..07ff90e --- /dev/null +++ b/android-cli/base/SKILL.md @@ -0,0 +1,207 @@ +--- +name: base +description: Orchestrates Android development tasks +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - AndroidCLI +--- + +including project creation, deployment, SDK management, and environment +diagnostics using the `android` command-line tool. + +This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. + +## SDK management + +To manage the installation of Android SDKs and tools, use the `sdk` command. For example: + +- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` +- `android sdk update []`: Update a specific package or all packages to the latest version. +- `android sdk remove `: Remove a package from the local SDK. +- `android sdk list --all`: List installed and available SDK packages. + +## Project creation + +Create projects from templates using the `create` command. + +For example: `android create empty-activity --name="My App" --output=./my-app` + +## Interacting with devices + +For more information on interacting with running devices, see [here](https://developer.android.com/agents/skills/android-cli/base/references/interact) + +## Running journey tests + +For more information on running journeys, see [here](https://developer.android.com/agents/skills/android-cli/base/references/journeys) + +## Doc searching + +The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. +By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. +Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. + +Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: +- Finding migration guides for APIs. +- Finding examples for APIs. +- Finding up-to-date information about Android APIs. +- Finding best practices for Android concepts. + +## Running APKs + +Use the `run` command to run Android apps. + +## Managing emulators + +Manage Android Virtual Devices (AVDs) using the `android emulator` command + +## Capturing screenshots + +Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. + +## Managing skills + +Manage antigravity agent skills for Android using the `android skills` command. + +## Inspecting UI Layouts + +Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. + +## Updating the CLI + +Update the Android CLI using the `android update` command. + +# `android help` output + +Usage: android \[-hV\] \[--sdk=PARAM\] \[COMMAND\] +-h, --help Show this help message and exit. +--sdk=PARAM Path to the Android SDK +-V, --version Print version information and exit. +Commands: +create Create a new Android project +describe Analyzes an Android project to generate descriptive metadata. +docs Android documentation commands +emulator Emulator commands +help Shows the help of all commands +info Print environment information (SDK Location, etc.) +init Initializes the environment (eg. skills) for Android CLI. +layout Returns the layout tree of an application +run Deploy an Android Application +screen Commands to view the device +sdk Download and list SDK packages +skills Manage skills +update Update the Android CLI + +create +Usage: android create \[-h\] \[--verbose\] \[--list\] \[--minSdk=api\] +--name=applicationName \[-o=dest-path\] \[template-name\] +Create a new Android project +\[template-name\] The template name +-h, --help Show this help message and exit. +--minSdk=api The 'minSdk' supported by the application (default +is defined in the template) +--name=applicationName +The name of the application (e.g. 'My Application') +-o, --output=dest-path The destination project directory path (default is +'.') +--verbose Enables verbose output +--list List all available templates + +describe +Usage: android describe \[-hV\] \[--project_dir=PARAM\] +Analyzes an Android project to generate descriptive metadata. +This command identifies and outputs the paths to JSON files that detail the +project's structure, including build targets and their corresponding output +artifact locations (e.g., APKs). This information enables other tools and +commands to locate build artifacts efficiently. +-h, --help Show this help message and exit. +--project_dir=PARAM The project directory to describe +-V, --version Print version information and exit. + +docs +Usage: android docs \[-h\] \[COMMAND\] +Android documentation commands +-h, --help Show this help message and exit. +Commands: +search Search Android documentation +fetch Fetch Android documentation + +emulator +Usage: android emulator \[-h\] \[COMMAND\] +Emulator commands +-h, --help Show this help message and exit. +Commands: +create Creates a virtual device +start Launches the specified virtual device. This command will return when +the emulator is fully started and ready to use. +stop Stops the specified virtual device +list Lists available virtual devices +remove Delete a virtual device + +help +Usage: android help \[COMMAND\] +Shows the help of all commands +\[COMMAND\] The command to show help for + +info +Usage: android info Print environment information (SDK Location, etc.) The specific field to print the value of. If omitted print all. + +init +Usage: android init +Initializes the environment (eg. skills) for Android CLI. + +layout +Usage: android layout \[-dhp\] \[--device=PARAM\] \[-o=PARAM\] +Returns the layout tree of an application +-d, --diff Returns a flat list of the layout elements that have +changed since the last invocation of ui-dump +--device=PARAM The device serial number +-h, --help Show this help message and exit. +-o, --output=PARAM Writes the layout tree to the specified file or +directory. If omitted, prints the tree to standard +output +-p, --pretty Pretty-prints the returned JSON + +run +Usage: android run \[-h\] \[--debug\] \[--activity=PARAM\] \[--device=PARAM\] +\[--type=PARAM\] \[--apks=PARAM\[,PARAM...\]\]... +Deploy an Android Application +--activity=PARAM The activity name +--apks=PARAM\[,PARAM...\] +The paths to the APKs +--debug Run in debug mode +--device=PARAM The device serial number +-h, --help Show this help message and exit. +--type=PARAM The component type (ACTIVITY, SERVICE, etc.) + +screen +Usage: android screen \[-h\] \[COMMAND\] +Commands to view the device +-h, --help Show this help message and exit. +Commands: +capture Outputs the device screen to a PNG +resolve Target UI elements visually + +sdk +Usage: android sdk \[COMMAND\] +Download and list SDK packages +Commands: +install Install SDK packages +update Update one or all packages to the latest version +remove Remove a package from the SDK +list List installed and available SDK packages + +skills +Usage: android skills \[COMMAND\] +Manage skills +Commands: +add Install a skill +remove Remove a skill +list List available skills +find Find skills by keyword + +update +Usage: android update \[--url=PARAM\] +Update the Android CLI +--url=PARAM The URL to download the update from \ No newline at end of file diff --git a/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md deleted file mode 100644 index da8b23b..0000000 --- a/devtools/android-cli/SKILL.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -name: android-cli -description: Orchestrates Android development tasks including project creation, deployment, SDK management, and environment diagnostics using the `android` command-line tool. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - keywords: - - sdk - - emulator - - skills - - docs - - knowledge base - - project creation - - screenshots ---- -# Android CLI Specialist - -This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. - -## SDK management -To manage the installation of Android SDKs and tools, use the `sdk` command. For example: - -- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` -- `android sdk update []`: Update a specific package or all packages to the latest version. -- `android sdk remove `: Remove a package from the local SDK. -- `android sdk list --all`: List installed and available SDK packages. - -## Project creation -Create projects from templates using the `create` command. - -For example: `android create empty-activity --name="My App" --output=./my-app` - -## Interacting with devices -For more information on interacting with running devices, see [here](references/interact.md) - -## Running journey tests -For more information on running journeys, see [here](references/journeys.md) - -## Doc searching -The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. -By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. -Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. - -Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: - - Finding migration guides for APIs. - - Finding examples for APIs. - - Finding up-to-date information about Android APIs. - - Finding best practices for Android concepts. - -## Running APKs -Use the `run` command to run Android apps. - -## Managing emulators - -Manage Android Virtual Devices (AVDs) using the `android emulator` command - -## Capturing screenshots - -Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. - -## Managing skills - -Manage antigravity agent skills for Android using the `android skills` command. - -## Inspecting UI Layouts - -Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. - -## Updating the CLI - -Update the Android CLI using the `android update` command. - -# `android help` output - -Usage: android [-hV] [--sdk=PARAM] [COMMAND] - -h, --help Show this help message and exit. - --sdk=PARAM Path to the Android SDK - -V, --version Print version information and exit. -Commands: - create Create a new Android project - describe Analyzes an Android project to generate descriptive metadata. - docs Android documentation commands - emulator Emulator commands - help Shows the help of all commands - info Print environment information (SDK Location, etc.) - init Initializes the environment (eg. skills) for Android CLI. - layout Returns the layout tree of an application - run Deploy an Android Application - screen Commands to view the device - sdk Download and list SDK packages - skills Manage skills - update Update the Android CLI - -create - Usage: android create [-h] [--verbose] [--list] [--minSdk=api] - --name=applicationName [-o=dest-path] [template-name] - Create a new Android project - [template-name] The template name - -h, --help Show this help message and exit. - --minSdk=api The 'minSdk' supported by the application (default - is defined in the template) - --name=applicationName - The name of the application (e.g. 'My Application') - -o, --output=dest-path The destination project directory path (default is - '.') - --verbose Enables verbose output - --list List all available templates - -describe - Usage: android describe [-hV] [--project_dir=PARAM] - Analyzes an Android project to generate descriptive metadata. - This command identifies and outputs the paths to JSON files that detail the - project's structure, including build targets and their corresponding output - artifact locations (e.g., APKs). This information enables other tools and - commands to locate build artifacts efficiently. - -h, --help Show this help message and exit. - --project_dir=PARAM The project directory to describe - -V, --version Print version information and exit. - -docs - Usage: android docs [-h] [COMMAND] - Android documentation commands - -h, --help Show this help message and exit. - Commands: - search Search Android documentation - fetch Fetch Android documentation - -emulator - Usage: android emulator [-h] [COMMAND] - Emulator commands - -h, --help Show this help message and exit. - Commands: - create Creates a virtual device - start Launches the specified virtual device. This command will return when - the emulator is fully started and ready to use. - stop Stops the specified virtual device - list Lists available virtual devices - remove Delete a virtual device - -help - Usage: android help [COMMAND] - Shows the help of all commands - [COMMAND] The command to show help for - -info - Usage: android info - Print environment information (SDK Location, etc.) - The specific field to print the value of. If omitted print all. - -init - Usage: android init - Initializes the environment (eg. skills) for Android CLI. - -layout - Usage: android layout [-dhp] [--device=PARAM] [-o=PARAM] - Returns the layout tree of an application - -d, --diff Returns a flat list of the layout elements that have - changed since the last invocation of ui-dump - --device=PARAM The device serial number - -h, --help Show this help message and exit. - -o, --output=PARAM Writes the layout tree to the specified file or - directory. If omitted, prints the tree to standard - output - -p, --pretty Pretty-prints the returned JSON - -run - Usage: android run [-h] [--debug] [--activity=PARAM] [--device=PARAM] - [--type=PARAM] [--apks=PARAM[,PARAM...]]... - Deploy an Android Application - --activity=PARAM The activity name - --apks=PARAM[,PARAM...] - The paths to the APKs - --debug Run in debug mode - --device=PARAM The device serial number - -h, --help Show this help message and exit. - --type=PARAM The component type (ACTIVITY, SERVICE, etc.) - -screen - Usage: android screen [-h] [COMMAND] - Commands to view the device - -h, --help Show this help message and exit. - Commands: - capture Outputs the device screen to a PNG - resolve Target UI elements visually - -sdk - Usage: android sdk [COMMAND] - Download and list SDK packages - Commands: - install Install SDK packages - update Update one or all packages to the latest version - remove Remove a package from the SDK - list List installed and available SDK packages - -skills - Usage: android skills [COMMAND] - Manage skills - Commands: - add Install a skill - remove Remove a skill - list List available skills - find Find skills by keyword - -update - Usage: android update [--url=PARAM] - Update the Android CLI - --url=PARAM The URL to download the update from - diff --git a/devtools/android-cli/references/interact.md b/devtools/android-cli/references/interact.md deleted file mode 100644 index 099e66b..0000000 --- a/devtools/android-cli/references/interact.md +++ /dev/null @@ -1,83 +0,0 @@ -# Tools -Run `android layout --help` and `android screen --help`. - -## UI Dump -`android layout` returns a flat JSON list of the UI elements on screen. -`android layout --diff` returns a flat JSON list of the UI elements that have changed since the last call to `layout` or `layout --diff` - -Each JSON object represents a UI element in the Android app. The following properties may be present: -- `text` - any literal text the element contains -- `resourceId` - the Android resource id used to refer to the element -- `contentDesc` - a description of a UI element for use by accessibility tools -- `interactions` - the set of user interactions the element supports. May contain one or more of: `checkable`, `clickable`, `focusable`, `scrollable`, `long-clickable`, `password` -- `state` - the set of states the element is in. May contain one or more of `checked`, `focused`, `selected` -- `bounds` - the screen coordinates of the bounding rectangle of the element, in the format `[min X,min Y][max X, max Y]` -- `center` - the screen coordinates of the center of the element, in the format `[x,y]` -- `off-screen` - if true, the element is in the UI hierarchy but not visible; it may require scrolling to view. - -Use `layout` as a primary means of examining an Android app. Use `layout --diff` to focus on changes and to keep your context small. -Example: When entering digits into a calculator, use `layout --diff` to output only the digit readout element. - -`layout` may fail due to the app displaying a WebView or animation; in these cases, use `android screen --annotate` to inspect the app. -This failure will likely resolve after navigating away from the current screen. - -## Screenshot -`android screen capture -o ` saves a PNG of the current device screen to `` - -Use `screen capture` as a secondary means of examining an Android app -Examples: -- Understanding the content of an on-screen image -- Looking at a `WebView` (web content does not always appear in the ui dump) -- Trying to find a UI element by its visual appearance - -**IMPORTANT**: Always *VISUALLY* examine the PNG image returned from `android screen` BEFORE doing anything else. - -## Annotated Screenshot -`android screen capture --annotate -o ` -`android screen resolve --screen --string ` - -The `--annotate` command adds numerical labels and bounding boxes around UI elements. Use this command to locate UI elements that cannot -be located in the `layout` output. - -**IMPORTANT**: When using `android screen --annotate`, always *VISUALLY* examine the resulting PNG file. - -To refer to these labels in input commands, use `screen resolve` to convert labels into coordinates: - -`android screen resolve --screen --string "#3"` returns ` ` - -To save turns, you can combine shell commands: - -`adb shell input $(android screen resolve --screen screen.png --string "tap #34")` - -This command taps on region #34 from `screen.png` - -## Input -Use `adb shell input` for interacting with Android devices. -Refer to the `"interactions"` property of an element for what interactions can be performed on a particular element. - -Interact with UI elements with their `center` coordinate or their `bounds` coordinates: -```json -{ - "key": -248568265, - "class": "android.widget.Button", - "bounds": "[138,9][167,38]", - "center": "[152,23]" -} -``` -To tap on this button, you would execute `adb shell input tap 152 23`. This taps the center. - -```json -{ - "key": 12487234, - "class": "com.example.ui.ScrollableList", - "bounds": "[100,200][400,600]", - "center": "[250,400]" -} -``` -To scroll down on this list, you would execute `adb shell input swipe 250 400 600 500`. This swipes from the center to the bottom over 500ms. - -# Android Interaction Rules -1. Always ensure text input fields have `"focused"` in their `"state"` list before entering text -2. If an element has `"scrollable"` in its `"interactions"` list, try scrolling it when looking for missing UI elements -2. Always scroll slowly when executing scroll inputs. The 5th argument to `adb shell input swipe` controls scroll duration. -3. Content may take time to load; if a `layout` is missing information after you take an action, wait a few seconds, then perform `layout --diff` to see if anything changes. \ No newline at end of file diff --git a/devtools/android-cli/references/journeys.md b/devtools/android-cli/references/journeys.md deleted file mode 100644 index 7ed45ad..0000000 --- a/devtools/android-cli/references/journeys.md +++ /dev/null @@ -1,97 +0,0 @@ -A journey is an XML-specified test of an Android app's behavior. It consists of a list of `` elements. For example: -```xml - - - A sample journey to illustrate the format - - - - Tap the "Home" icon - - - Verify that the app is on its Home screen - - - -``` - -Evaluate a journey by proceeding through the `` list in sequential order. Evaluate each `` block individually. -A journey succeeds if all elements in the `` list succeed. - -A journey is a test case for an app. The journey XML is the source of truth; if the app disagrees with the journey, the app has failed. -Additionally, if the app exits, crashes, or freezes, journey evaluation stops and the journey fails. - -**IMPORTANT** - Execute each step EXACTLY as written, and independently of other steps! If an action says to `"tap the first search result"`, -you MUST find the search results and tap the first one. Do this even if you believe you know the intent behind the action. - -## Taking Actions -Some `` elements specify UI interactions to perform on the running Android app. Perform the interaction and verify that the app does -not crash or behave in an unexpected manner. This is the *only* verification you should perform for an ``. - -If the interaction cannot be performed as specified, the journey fails. -Example: -```Click the red button``` -If you determine a red button is not present in the UI, the journey fails. - -If the text of an `` specifies a list of actions, break it into sub-actions and evaluate them individually: -Example: -```Search for soda and add the first result to the cart``` -This should be evaluated as: -``` -Search for soda -Add the first result to the cart -``` - -If an `` contains something that is not a specification for a UI interaction, alert the user that the journey is malformed and exit -early, specifying the error in question. - -## Verifying Expectations -`` elements that begin with "check" or "verify" specify expectations for the current state of the Android app. Determine the current -state of the app and check if the expectations are met. - -Determine the current state of the app by inspecting the current screen of the device without interacting with it. -Example: -```Check if "Switch 2" is visible on the screen``` -This requires only inspecting the current screen, not scrolling or interacting. If "Switch 2" is not currently visible, the action fails. - -If the expectations are not met, mark the `` as a failure and the journey evaluation ends. A single `` may contain -multiple expectations. -Example: -```Verify that the app is on the Home screen, the Home icon is blue, and the temperature is displayed``` -This `` fails if ANY of the following are false: -- The app is on the Home screen -- There is a Home icon, and it is blue -- A temperature is displayed - -## Handling failure -When running a journey, evaluate it as a test. Failure is acceptable, and often expected. Proper reporting of failures is the priority. - -Keep debugging and troubleshooting to a minimum; assume that tools are showing you the correct output every time. The goal is to determine -if the *current* Android app can correctly handle the *current* steps outlined in the journey. Suggestions for bug fixes, clarification, or -other improvements should be kept to journey evaluation summary at the end. - -## Summarizing -For each `` you evaluated, output JSON describing the results. - -``` -{ - "journey:", The name of the journey - "results:" [ - { - // A string containing the full text of the - "action": "Click the blue button, - // "PASSED" if the instruction was evaluated, "FAILED" if the instruction could not be evaluated, or "SKIPPED" if journey evaluation ended early because an instruction failed - "status": "PASSED", - // A list of the ADB commands executed while evaluating the instruction, - "commands": [ "adb input swipe 490 200 500 500 500", "adb input tap 45 920" ], - // Failure reasons, feedback, or other useful information - "comment": "The journey step doesn't specify that the button requires scrolling to see", - }, - { - "action": "The home screen is shown", - "status": "FAILED", - "comment": "The settings page was shown", - }, - ] -} -``` From 3463433cfdd061155428852f2dcca9d0108bdeae Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Sat, 9 May 2026 10:47:43 +0000 Subject: [PATCH 14/61] Updates skills (2026-05-09 10:47) --- android-cli/base/SKILL.md | 207 - build/agp/agp-9-upgrade/SKILL.md | 3 +- camera/camera1-to-camerax/SKILL.md | 60 +- devtools/android-cli/SKILL.md | 207 + devtools/android-cli/references/interact.md | 83 + devtools/android-cli/references/journeys.md | 97 + .../SKILL.md | 3 +- .../migrate-xml-theme-to-compose.md | 25 +- navigation/navigation-3/SKILL.md | 3 +- performance/r8-analyzer/SKILL.md | 3 +- .../SKILL.md | 3 +- profilers/perfetto-sql/SKILL.md | 146 + .../references/perfetto-stdlib-docs.md | 7044 +++++++++++++++++ system/edge-to-edge/SKILL.md | 3 +- .../SKILL.md | 3 +- .../ai-glasses/first-activity.md | 4 +- .../references/button-samples-source.md | 4 +- .../references/button-source.md | 4 +- .../references/card-samples-source.md | 4 +- .../references/card-source.md | 12 +- .../references/listitem-source.md | 16 +- .../references/stack-source.md | 1 + .../references/titlechip-samples-source.md | 4 +- .../references/titlechip-source.md | 2 +- 24 files changed, 7663 insertions(+), 278 deletions(-) delete mode 100644 android-cli/base/SKILL.md create mode 100644 devtools/android-cli/SKILL.md create mode 100644 devtools/android-cli/references/interact.md create mode 100644 devtools/android-cli/references/journeys.md create mode 100644 profilers/perfetto-sql/SKILL.md create mode 100644 profilers/perfetto-sql/references/perfetto-stdlib-docs.md diff --git a/android-cli/base/SKILL.md b/android-cli/base/SKILL.md deleted file mode 100644 index 07ff90e..0000000 --- a/android-cli/base/SKILL.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -name: base -description: Orchestrates Android development tasks -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - keywords: - - AndroidCLI ---- - -including project creation, deployment, SDK management, and environment -diagnostics using the `android` command-line tool. - -This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. - -## SDK management - -To manage the installation of Android SDKs and tools, use the `sdk` command. For example: - -- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` -- `android sdk update []`: Update a specific package or all packages to the latest version. -- `android sdk remove `: Remove a package from the local SDK. -- `android sdk list --all`: List installed and available SDK packages. - -## Project creation - -Create projects from templates using the `create` command. - -For example: `android create empty-activity --name="My App" --output=./my-app` - -## Interacting with devices - -For more information on interacting with running devices, see [here](https://developer.android.com/agents/skills/android-cli/base/references/interact) - -## Running journey tests - -For more information on running journeys, see [here](https://developer.android.com/agents/skills/android-cli/base/references/journeys) - -## Doc searching - -The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. -By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. -Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. - -Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: -- Finding migration guides for APIs. -- Finding examples for APIs. -- Finding up-to-date information about Android APIs. -- Finding best practices for Android concepts. - -## Running APKs - -Use the `run` command to run Android apps. - -## Managing emulators - -Manage Android Virtual Devices (AVDs) using the `android emulator` command - -## Capturing screenshots - -Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. - -## Managing skills - -Manage antigravity agent skills for Android using the `android skills` command. - -## Inspecting UI Layouts - -Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. - -## Updating the CLI - -Update the Android CLI using the `android update` command. - -# `android help` output - -Usage: android \[-hV\] \[--sdk=PARAM\] \[COMMAND\] --h, --help Show this help message and exit. ---sdk=PARAM Path to the Android SDK --V, --version Print version information and exit. -Commands: -create Create a new Android project -describe Analyzes an Android project to generate descriptive metadata. -docs Android documentation commands -emulator Emulator commands -help Shows the help of all commands -info Print environment information (SDK Location, etc.) -init Initializes the environment (eg. skills) for Android CLI. -layout Returns the layout tree of an application -run Deploy an Android Application -screen Commands to view the device -sdk Download and list SDK packages -skills Manage skills -update Update the Android CLI - -create -Usage: android create \[-h\] \[--verbose\] \[--list\] \[--minSdk=api\] ---name=applicationName \[-o=dest-path\] \[template-name\] -Create a new Android project -\[template-name\] The template name --h, --help Show this help message and exit. ---minSdk=api The 'minSdk' supported by the application (default -is defined in the template) ---name=applicationName -The name of the application (e.g. 'My Application') --o, --output=dest-path The destination project directory path (default is -'.') ---verbose Enables verbose output ---list List all available templates - -describe -Usage: android describe \[-hV\] \[--project_dir=PARAM\] -Analyzes an Android project to generate descriptive metadata. -This command identifies and outputs the paths to JSON files that detail the -project's structure, including build targets and their corresponding output -artifact locations (e.g., APKs). This information enables other tools and -commands to locate build artifacts efficiently. --h, --help Show this help message and exit. ---project_dir=PARAM The project directory to describe --V, --version Print version information and exit. - -docs -Usage: android docs \[-h\] \[COMMAND\] -Android documentation commands --h, --help Show this help message and exit. -Commands: -search Search Android documentation -fetch Fetch Android documentation - -emulator -Usage: android emulator \[-h\] \[COMMAND\] -Emulator commands --h, --help Show this help message and exit. -Commands: -create Creates a virtual device -start Launches the specified virtual device. This command will return when -the emulator is fully started and ready to use. -stop Stops the specified virtual device -list Lists available virtual devices -remove Delete a virtual device - -help -Usage: android help \[COMMAND\] -Shows the help of all commands -\[COMMAND\] The command to show help for - -info -Usage: android info Print environment information (SDK Location, etc.) The specific field to print the value of. If omitted print all. - -init -Usage: android init -Initializes the environment (eg. skills) for Android CLI. - -layout -Usage: android layout \[-dhp\] \[--device=PARAM\] \[-o=PARAM\] -Returns the layout tree of an application --d, --diff Returns a flat list of the layout elements that have -changed since the last invocation of ui-dump ---device=PARAM The device serial number --h, --help Show this help message and exit. --o, --output=PARAM Writes the layout tree to the specified file or -directory. If omitted, prints the tree to standard -output --p, --pretty Pretty-prints the returned JSON - -run -Usage: android run \[-h\] \[--debug\] \[--activity=PARAM\] \[--device=PARAM\] -\[--type=PARAM\] \[--apks=PARAM\[,PARAM...\]\]... -Deploy an Android Application ---activity=PARAM The activity name ---apks=PARAM\[,PARAM...\] -The paths to the APKs ---debug Run in debug mode ---device=PARAM The device serial number --h, --help Show this help message and exit. ---type=PARAM The component type (ACTIVITY, SERVICE, etc.) - -screen -Usage: android screen \[-h\] \[COMMAND\] -Commands to view the device --h, --help Show this help message and exit. -Commands: -capture Outputs the device screen to a PNG -resolve Target UI elements visually - -sdk -Usage: android sdk \[COMMAND\] -Download and list SDK packages -Commands: -install Install SDK packages -update Update one or all packages to the latest version -remove Remove a package from the SDK -list List installed and available SDK packages - -skills -Usage: android skills \[COMMAND\] -Manage skills -Commands: -add Install a skill -remove Remove a skill -list List available skills -find Find skills by keyword - -update -Usage: android update \[--url=PARAM\] -Update the Android CLI ---url=PARAM The URL to download the update from \ No newline at end of file diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build/agp/agp-9-upgrade/SKILL.md index c7b4f76..e39dd14 100644 --- a/build/agp/agp-9-upgrade/SKILL.md +++ b/build/agp/agp-9-upgrade/SKILL.md @@ -6,6 +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-04-17' keywords: - Android Gradle Plugin 9 - AGP 9 @@ -99,4 +100,4 @@ 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. \ No newline at end of file +[references/paparazzi-gradle-9.md](references/paparazzi-gradle-9.md) for details. diff --git a/camera/camera1-to-camerax/SKILL.md b/camera/camera1-to-camerax/SKILL.md index 52ce24a..b47f97c 100644 --- a/camera/camera1-to-camerax/SKILL.md +++ b/camera/camera1-to-camerax/SKILL.md @@ -6,6 +6,7 @@ description: Use this skill to migrate legacy Android camera implementations (Ca license: Complete terms in LICENSE.txt metadata: author: Google LLC + last-updated: '2026-05-06' keywords: - Android - CameraX @@ -24,43 +25,46 @@ for interoperability, or version 1.5.0 or higher for Compose extensions. If you are using a Version Catalog (`libs.versions.toml`), add the following: - [versions] - camerax = "" - [libraries] - androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } - androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } - androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } - # If using Android Views (Option A) - androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } - # If using Jetpack Compose (Option B) - androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" } +```kotlin +[versions] +camerax = "" + +[libraries] +androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } +androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } +androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } +androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" } +``` + +
And in your `build.gradle.kts` (or `build.gradle`): - // Core dependencies - implementation(libs.androidx.camera.core) - implementation(libs.androidx.camera.camera2) - implementation(libs.androidx.camera.lifecycle) - // If using Android Views (Option A) - implementation(libs.androidx.camera.view) +```kotlin +implementation(libs.androidx.camera.core) +implementation(libs.androidx.camera.camera2) +implementation(libs.androidx.camera.lifecycle) +implementation(libs.androidx.camera.view) +implementation(libs.androidx.camera.compose) +``` - // If using Jetpack Compose (Option B) - implementation(libs.androidx.camera.compose) +
Without a Version Catalog, fall back to these standard Gradle dependencies: - // Core dependencies - implementation "androidx.camera:camera-core:" - implementation "androidx.camera:camera-camera2:" - implementation "androidx.camera:camera-lifecycle:" - // If using Android Views (Option A) - implementation "androidx.camera:camera-view:" +```kotlin +implementation "androidx.camera:camera-core:" +implementation "androidx.camera:camera-camera2:" +implementation "androidx.camera:camera-lifecycle:" +implementation "androidx.camera:camera-view:" +implementation "androidx.camera:camera-compose:" +``` - // If using Jetpack Compose (Option B) - implementation "androidx.camera:camera-compose:" +
## Step 1: Remove Legacy Implementation @@ -258,9 +262,9 @@ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
-## What not to do. +## Constraints - **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`. - **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations. - **Don't forget to close the ImageProxy** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos. -- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an old fallback option that introduces resizing issues. \ No newline at end of file +- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an old fallback option that introduces resizing issues. diff --git a/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md new file mode 100644 index 0000000..595401d --- /dev/null +++ b/devtools/android-cli/SKILL.md @@ -0,0 +1,207 @@ +--- +name: android-cli +description: Orchestrates Android development tasks including project creation, deployment, SDK management, and environment diagnostics using the `android` command-line tool. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + keywords: + - sdk + - emulator + - skills + - docs + - knowledge base + - project creation + - screenshots +--- +# Android CLI Specialist + +This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. + +## SDK management +To manage the installation of Android SDKs and tools, use the `sdk` command. For example: + +- `android sdk install [@]...`: Install specific packages. Multiple packages can be specified, separated by spaces. `` defaults to latest. For example: `android sdk install platforms/android-30@2 platforms/android-34` +- `android sdk update []`: Update a specific package or all packages to the latest version. +- `android sdk remove `: Remove a package from the local SDK. +- `android sdk list --all`: List installed and available SDK packages. + +## Project creation +Create projects from templates using the `create` command. + +For example: `android create empty-activity --name="My App" --output=./my-app` + +## Interacting with devices +For more information on interacting with running devices, see [here](references/interact.md) + +## Running journey tests +For more information on running journeys, see [here](references/journeys.md) + +## Doc searching +The `docs` command searches authoritative, high-quality Android developer documentation in the Android Knowledge Base. +By providing a few keywords, this tool will return high quality articles that contain examples or guidance on how to use Android APIs or libraries. +Use this tool to obtain additional information on how to achieve Android-specific tasks or to know more about Android APIs, surfaces, libraries, or devices. + +Always use this tool to get the most up-to-date information about Android concepts. Typical good use cases are: + - Finding migration guides for APIs. + - Finding examples for APIs. + - Finding up-to-date information about Android APIs. + - Finding best practices for Android concepts. + +## Running APKs +Use the `run` command to run Android apps. + +## Managing emulators + +Manage Android Virtual Devices (AVDs) using the `android emulator` command + +## Capturing screenshots + +Capture an image of the current screen of a connected Android device and output it to a file using the `android screenshot` command. + +## Managing skills + +Manage antigravity agent skills for Android using the `android skills` command. + +## Inspecting UI Layouts + +Use the `android layout` command to inspect the UI layout of an Android application. It returns the layout tree of an Android application in JSON format. When debugging UI errors, this is often a much faster approach than taking a screenshot. + +## Updating the CLI + +Update the Android CLI using the `android update` command. + +# `android help` output + +Usage: android [-hV] [--sdk=PARAM] [COMMAND] + -h, --help Show this help message and exit. + --sdk=PARAM Path to the Android SDK + -V, --version Print version information and exit. +Commands: + create Create a new Android project + describe Analyzes an Android project to generate descriptive metadata. + docs Android documentation commands + emulator Emulator commands + help Shows the help of all commands + info Print environment information (SDK Location, etc.) + init Initializes the environment (eg. skills) for Android CLI. + layout Returns the layout tree of an application + run Deploy an Android Application + screen Commands to view the device + sdk Download and list SDK packages + skills Manage skills + update Update the Android CLI + +create + Usage: android create [-h] [--verbose] [--list] [--minSdk=api] + --name=applicationName [-o=dest-path] [template-name] + Create a new Android project + [template-name] The template name + -h, --help Show this help message and exit. + --minSdk=api The 'minSdk' supported by the application (default + is defined in the template) + --name=applicationName + The name of the application (e.g. 'My Application') + -o, --output=dest-path The destination project directory path (default is + '.') + --verbose Enables verbose output + --list List all available templates + +describe + Usage: android describe [-hV] [--project_dir=PARAM] + Analyzes an Android project to generate descriptive metadata. + This command identifies and outputs the paths to JSON files that detail the + project's structure, including build targets and their corresponding output + artifact locations (e.g., APKs). This information enables other tools and + commands to locate build artifacts efficiently. + -h, --help Show this help message and exit. + --project_dir=PARAM The project directory to describe + -V, --version Print version information and exit. + +docs + Usage: android docs [-h] [COMMAND] + Android documentation commands + -h, --help Show this help message and exit. + Commands: + search Search Android documentation + fetch Fetch Android documentation + +emulator + Usage: android emulator [-h] [COMMAND] + Emulator commands + -h, --help Show this help message and exit. + Commands: + create Creates a virtual device + start Launches the specified virtual device. This command will return when + the emulator is fully started and ready to use. + stop Stops the specified virtual device + list Lists available virtual devices + remove Delete a virtual device + +help + Usage: android help [COMMAND] + Shows the help of all commands + [COMMAND] The command to show help for + +info + Usage: android info + Print environment information (SDK Location, etc.) + The specific field to print the value of. If omitted print all. + +init + Usage: android init + Initializes the environment (eg. skills) for Android CLI. + +layout + Usage: android layout [-dhp] [--device=PARAM] [-o=PARAM] + Returns the layout tree of an application + -d, --diff Returns a flat list of the layout elements that have + changed since the last invocation of ui-dump + --device=PARAM The device serial number + -h, --help Show this help message and exit. + -o, --output=PARAM Writes the layout tree to the specified file or + directory. If omitted, prints the tree to standard + output + -p, --pretty Pretty-prints the returned JSON + +run + Usage: android run [-h] [--debug] [--activity=PARAM] [--device=PARAM] + [--type=PARAM] [--apks=PARAM[,PARAM...]]... + Deploy an Android Application + --activity=PARAM The activity name + --apks=PARAM[,PARAM...] + The paths to the APKs + --debug Run in debug mode + --device=PARAM The device serial number + -h, --help Show this help message and exit. + --type=PARAM The component type (ACTIVITY, SERVICE, etc.) + +screen + Usage: android screen [-h] [COMMAND] + Commands to view the device + -h, --help Show this help message and exit. + Commands: + capture Outputs the device screen to a PNG + resolve Target UI elements visually + +sdk + Usage: android sdk [COMMAND] + Download and list SDK packages + Commands: + install Install SDK packages + update Update one or all packages to the latest version + remove Remove a package from the SDK + list List installed and available SDK packages + +skills + Usage: android skills [COMMAND] + Manage skills + Commands: + add Install a skill + remove Remove a skill + list List available skills + find Find skills by keyword + +update + Usage: android update [--url=PARAM] + Update the Android CLI + --url=PARAM The URL to download the update from \ No newline at end of file diff --git a/devtools/android-cli/references/interact.md b/devtools/android-cli/references/interact.md new file mode 100644 index 0000000..099e66b --- /dev/null +++ b/devtools/android-cli/references/interact.md @@ -0,0 +1,83 @@ +# Tools +Run `android layout --help` and `android screen --help`. + +## UI Dump +`android layout` returns a flat JSON list of the UI elements on screen. +`android layout --diff` returns a flat JSON list of the UI elements that have changed since the last call to `layout` or `layout --diff` + +Each JSON object represents a UI element in the Android app. The following properties may be present: +- `text` - any literal text the element contains +- `resourceId` - the Android resource id used to refer to the element +- `contentDesc` - a description of a UI element for use by accessibility tools +- `interactions` - the set of user interactions the element supports. May contain one or more of: `checkable`, `clickable`, `focusable`, `scrollable`, `long-clickable`, `password` +- `state` - the set of states the element is in. May contain one or more of `checked`, `focused`, `selected` +- `bounds` - the screen coordinates of the bounding rectangle of the element, in the format `[min X,min Y][max X, max Y]` +- `center` - the screen coordinates of the center of the element, in the format `[x,y]` +- `off-screen` - if true, the element is in the UI hierarchy but not visible; it may require scrolling to view. + +Use `layout` as a primary means of examining an Android app. Use `layout --diff` to focus on changes and to keep your context small. +Example: When entering digits into a calculator, use `layout --diff` to output only the digit readout element. + +`layout` may fail due to the app displaying a WebView or animation; in these cases, use `android screen --annotate` to inspect the app. +This failure will likely resolve after navigating away from the current screen. + +## Screenshot +`android screen capture -o ` saves a PNG of the current device screen to `` + +Use `screen capture` as a secondary means of examining an Android app +Examples: +- Understanding the content of an on-screen image +- Looking at a `WebView` (web content does not always appear in the ui dump) +- Trying to find a UI element by its visual appearance + +**IMPORTANT**: Always *VISUALLY* examine the PNG image returned from `android screen` BEFORE doing anything else. + +## Annotated Screenshot +`android screen capture --annotate -o ` +`android screen resolve --screen --string ` + +The `--annotate` command adds numerical labels and bounding boxes around UI elements. Use this command to locate UI elements that cannot +be located in the `layout` output. + +**IMPORTANT**: When using `android screen --annotate`, always *VISUALLY* examine the resulting PNG file. + +To refer to these labels in input commands, use `screen resolve` to convert labels into coordinates: + +`android screen resolve --screen --string "#3"` returns ` ` + +To save turns, you can combine shell commands: + +`adb shell input $(android screen resolve --screen screen.png --string "tap #34")` + +This command taps on region #34 from `screen.png` + +## Input +Use `adb shell input` for interacting with Android devices. +Refer to the `"interactions"` property of an element for what interactions can be performed on a particular element. + +Interact with UI elements with their `center` coordinate or their `bounds` coordinates: +```json +{ + "key": -248568265, + "class": "android.widget.Button", + "bounds": "[138,9][167,38]", + "center": "[152,23]" +} +``` +To tap on this button, you would execute `adb shell input tap 152 23`. This taps the center. + +```json +{ + "key": 12487234, + "class": "com.example.ui.ScrollableList", + "bounds": "[100,200][400,600]", + "center": "[250,400]" +} +``` +To scroll down on this list, you would execute `adb shell input swipe 250 400 600 500`. This swipes from the center to the bottom over 500ms. + +# Android Interaction Rules +1. Always ensure text input fields have `"focused"` in their `"state"` list before entering text +2. If an element has `"scrollable"` in its `"interactions"` list, try scrolling it when looking for missing UI elements +2. Always scroll slowly when executing scroll inputs. The 5th argument to `adb shell input swipe` controls scroll duration. +3. Content may take time to load; if a `layout` is missing information after you take an action, wait a few seconds, then perform `layout --diff` to see if anything changes. \ No newline at end of file diff --git a/devtools/android-cli/references/journeys.md b/devtools/android-cli/references/journeys.md new file mode 100644 index 0000000..74545dd --- /dev/null +++ b/devtools/android-cli/references/journeys.md @@ -0,0 +1,97 @@ +A journey is an XML-specified test of an Android app's behavior. It consists of a list of `` elements. For example: +```xml + + + A sample journey to illustrate the format + + + + Tap the "Home" icon + + + Verify that the app is on its Home screen + + + +``` + +Evaluate a journey by proceeding through the `` list in sequential order. Evaluate each `` block individually. +A journey succeeds if all elements in the `` list succeed. + +A journey is a test case for an app. The journey XML is the source of truth; if the app disagrees with the journey, the app has failed. +Additionally, if the app exits, crashes, or freezes, journey evaluation stops and the journey fails. + +**IMPORTANT** - Execute each step EXACTLY as written, and independently of other steps! If an action says to `"tap the first search result"`, +you MUST find the search results and tap the first one. Do this even if you believe you know the intent behind the action. + +## Taking Actions +Some `` elements specify UI interactions to perform on the running Android app. Perform the interaction and verify that the app does +not crash or behave in an unexpected manner. This is the *only* verification you should perform for an ``. + +If the interaction cannot be performed as specified, the journey fails. +Example: +```Click the red button``` +If you determine a red button is not present in the UI, the journey fails. + +If the text of an `` specifies a list of actions, break it into sub-actions and evaluate them individually: +Example: +```Search for soda and add the first result to the cart``` +This should be evaluated as: +``` +Search for soda +Add the first result to the cart +``` + +If an `` contains something that is not a specification for a UI interaction, alert the user that the journey is malformed and exit +early, specifying the error in question. + +## Verifying Expectations +`` elements that begin with "check" or "verify" specify expectations for the current state of the Android app. Determine the current +state of the app and check if the expectations are met. + +Determine the current state of the app by inspecting the current screen of the device without interacting with it. +Example: +```Check if "Switch 2" is visible on the screen``` +This requires only inspecting the current screen, not scrolling or interacting. If "Switch 2" is not currently visible, the action fails. + +If the expectations are not met, mark the `` as a failure and the journey evaluation ends. A single `` may contain +multiple expectations. +Example: +```Verify that the app is on the Home screen, the Home icon is blue, and the temperature is displayed``` +This `` fails if ANY of the following are false: +- The app is on the Home screen +- There is a Home icon, and it is blue +- A temperature is displayed + +## Handling failure +When running a journey, evaluate it as a test. Failure is acceptable, and often expected. Proper reporting of failures is the priority. + +Keep debugging and troubleshooting to a minimum; assume that tools are showing you the correct output every time. The goal is to determine +if the *current* Android app can correctly handle the *current* steps outlined in the journey. Suggestions for bug fixes, clarification, or +other improvements should be kept to journey evaluation summary at the end. + +## Summarizing +For each `` you evaluated, output JSON describing the results. + +``` +{ + "journey:", The name of the journey + "results:" [ + { + // A string containing the full text of the + "action": "Click the blue button, + // "PASSED" if the instruction was evaluated, "FAILED" if the instruction could not be evaluated, or "SKIPPED" if journey evaluation ended early because an instruction failed + "status": "PASSED", + // A list of the ADB commands executed while evaluating the instruction, + "commands": [ "adb input swipe 490 200 500 500 500", "adb input tap 45 920" ], + // Failure reasons, feedback, or other useful information + "comment": "The journey step doesn't specify that the button requires scrolling to see", + }, + { + "action": "The home screen is shown", + "status": "FAILED", + "comment": "The settings page was shown", + }, + ] +} +``` \ 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 ed21187..6af64db 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,6 +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-05-06' keywords: - Jetpack Compose - migration @@ -120,4 +121,4 @@ 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. \ No newline at end of file +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/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 d8230f8..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 @@ -15,9 +15,9 @@ Material 3 Compose theming. | Term | Definition | |---|---| | `MaterialTheme` | The composable function that provides theming (colors, typography, shapes) to Compose UI components. | -| `Shape` | A Compose object used to define custom component shapes for a `MaterialTheme`. | +| `Shapes` | A Compose object used to define custom component shapes for a `MaterialTheme`. | | `Typography` | A Compose object used to define custom text styles (font families, sizes, weights) for a `MaterialTheme`. | -| `Color` | A Compose object used to define custom color schemes for `MaterialTheme`. | +| `ColorScheme` | A Compose object used to define custom color schemes for `MaterialTheme`. | | XML Theme | The Android theming system defined in XML files, used by the View system. | ## Limitations @@ -79,8 +79,8 @@ Material 3 Compose. - If your app uses custom shapes: - 1. In your Compose code, define a `Shape` object to replicate your XML shape definitions. - 2. Provide this `Shape` object to your `MaterialTheme`. + 1. In your Compose code, define a `Shapes` object to replicate your XML shape definitions. + 2. Provide this `Shapes` object to your `MaterialTheme`. For more details, see [shapes](https://developer.android.com/develop/ui/compose/designsystems/material3#shapes). - If your app uses custom typography: @@ -111,6 +111,7 @@ Material 3 Compose. ## Step 5: Migrate styles (styles.xml) XML styles (styles.xml) system defines styles and appearance of: + 1. Widgets, components, themes for windows and dialogs 2. Typography 3. Themes and overlays @@ -118,12 +119,14 @@ XML styles (styles.xml) system defines styles and appearance of: 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 to composables, 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 @@ -139,11 +142,11 @@ components. | XML | Compose | |---|---| -| `Theme.MaterialComponents.*` | `MaterialTheme(colorScheme, typography, shapes) { }` | +| `Theme.Material3.*` | `MaterialTheme(colorScheme, typography, shapes) { }` | | `TextAppearance.Material3.BodyMedium` | `TextStyle(...)` defined in `Typography(bodyMedium = ...)` | | `ShapeAppearance.*.SmallComponent` | `Shapes(small = RoundedCornerShape(X.dp))` | -| `Widget.MaterialComponents.Button` | `Button(colors = ButtonDefaults.buttonColors(...))` | -| `Widget.MaterialComponents.CardView` | `Card(shape=..., elevation=..., colors=...)` | +| `Widget.Material3.Button` | `Button(colors = ButtonDefaults.buttonColors(...))` | +| `Widget.Material3.CardView` | `Card(shape=..., elevation=..., colors=...)` | | `Widget.*.TextInputLayout.OutlinedBox` | `OutlinedTextField(colors = OutlinedTextFieldDefaults.colors(...))` | | `Widget.*.Chip.Filter` | `FilterChip(colors = FilterChipDefaults.filterChipColors(...))` | | `Widget.*.Toolbar.Primary` | `TopAppBar(colors = TopAppBarDefaults.topAppBarColors(...))` | @@ -160,7 +163,7 @@ components. ## 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 +of truth for the new Material Theme in Compose. Never invent new theme values during migration, to maintain brand consistency and avoid visual regressions. diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index 743782a..7c9afbb 100644 --- a/navigation/navigation-3/SKILL.md +++ b/navigation/navigation-3/SKILL.md @@ -8,6 +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-05-05' keywords: - recipe - Android @@ -106,4 +107,4 @@ Code examples showcasing common patterns. ### Returning results - *[Returning Results as Events](references/android/guide/navigation/navigation-3/recipes/results-event.md)* : Returning results as events to content in another `NavEntry` -- *[Returning Results as State](references/android/guide/navigation/navigation-3/recipes/results-state.md)* : Returning results as state stored in a `CompositionLocal` \ No newline at end of file +- *[Returning Results as State](references/android/guide/navigation/navigation-3/recipes/results-state.md)* : Returning results as state stored in a `CompositionLocal` diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index c2265ba..a661bc0 100644 --- a/performance/r8-analyzer/SKILL.md +++ b/performance/r8-analyzer/SKILL.md @@ -7,6 +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-04-25' keywords: - R8 - proguard @@ -37,4 +38,4 @@ metadata: - Don't mention the generated files. - Don't mention exceptions that occur during execution. - Don't mention the benefits of R8 -- Don't mention any files of this skill \ No newline at end of file +- Don't mention any files of this skill diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md index ac34277..2feb044 100644 --- a/play/play-billing-library-version-upgrade/SKILL.md +++ b/play/play-billing-library-version-upgrade/SKILL.md @@ -6,6 +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-04-13' keywords: - android - play billing @@ -89,4 +90,4 @@ Explain the "Why" to the developer: - "I updated your SDK to \[Version\] because PBL \[Version\] requires it for \[Reason from docs\]." - "I removed your custom `retryConnection()` logic because it is now handled natively by the library using `enableAutoServiceReconnection()`." -- "Successfully upgraded from PBL \[Old\] to PBL \[New\] and verified with unit tests. Based on an analysis of features in the latest library and this application's current feature set, I suggest exploring \[New Feature\] (e.g., Prepaid Plans or Installments) from the latest release because it is now available but not yet implemented." \ No newline at end of file +- "Successfully upgraded from PBL \[Old\] to PBL \[New\] and verified with unit tests. Based on an analysis of features in the latest library and this application's current feature set, I suggest exploring \[New Feature\] (e.g., Prepaid Plans or Installments) from the latest release because it is now available but not yet implemented." diff --git a/profilers/perfetto-sql/SKILL.md b/profilers/perfetto-sql/SKILL.md new file mode 100644 index 0000000..6bbd0d9 --- /dev/null +++ b/profilers/perfetto-sql/SKILL.md @@ -0,0 +1,146 @@ +--- +name: perfetto-sql +description: Translates natural language data intents into syntactically valid Perfetto + SQL queries and executes them against a local trace file. Use this skill to extract + slice, thread, or memory data from Android Perfetto traces using trace_processor. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-07' + keywords: + - Android + - Perfetto SQL + - Query Guidelines + - Performance Profiling + - Trace Analysis + - SQL Best Practices + - SPAN_JOIN + - Idempotency +--- + +## Guidelines and Hints + +- **Idempotency:** Ensure queries are idempotent to prevent "already exists" errors during multiple executions. + - For Perfetto objects, always use `CREATE OR REPLACE`: `CREATE OR REPLACE + PERFETTO TABLE`, `CREATE OR REPLACE PERFETTO VIEW`, `CREATE OR REPLACE + PERFETTO FUNCTION`, `CREATE OR REPLACE PERFETTO MACRO`. + - For SQLite Virtual Tables (such as `SPAN_JOIN`), `CREATE OR REPLACE` is not supported. Explicitly drop them first: `DROP TABLE IF EXISTS + my_table; CREATE VIRTUAL TABLE my_table USING SPAN_JOIN(...);` + - For standard SQLite indexes, prepend `DROP INDEX IF EXISTS index_name;`. +- `SPAN_JOIN` will crash if intervals **within the same input table** overlap. Always use the `PARTITIONED {column}` (for example, `PARTITIONED upid`) clause to isolate intervals. +- Intermediate tables fed into a `SPAN_JOIN` must be materialized using `CREATE PERFETTO TABLE`, not `CREATE VIEW`. +- **Trace Boundaries (`dur = -1`):** Slices or thread states that don't finish before the trace ends are recorded with `dur = -1`. When calculating a bounding box (for example, `ts + dur`) or summing durations (`SUM(dur)`), handle incomplete durations using: `IIF(dur = -1, trace_end() - ts, dur)`. +- **Robust State Transitions:** Avoid manual timestamp arithmetic (for example, `ts + dur = next.ts`) to join adjacent events. Rely on standard library modules (for example, `sched.runnable`, `linux.perf.counters`, `intervals.overlap`) which safely handle trace gaps and preemptions. +- **Unique Identifiers:** When writing SQL queries in Perfetto, you must join tables using `utid` (unique thread ID) or `upid` (unique process ID) instead of the regular `tid` or `pid`. **Why it's useful** : The operating system recycles `TIDs` and `PIDs`, while `UTIDs` and `UPIDs` remain unique for the lifetime of the trace, which prevents incorrect joins. +- **Safe Argument Extraction:** Use `EXTRACT_ARG(arg_set_id, 'key')` to extract dictionary or JSON-like properties from slices or tracks. Don't attempt string parsing. +- **String Matching (Always use GLOB):** Use `GLOB` instead of `LIKE`. `LIKE` causes performance bottlenecks and treats underscores (`_`) as wildcards, leading to bugs. + - **Exact matches:** Use `=`. + - **Substring matches:** Use `GLOB` with `*` (for example, `name GLOB + '*RenderThread*'`). + - **Case-insensitive matches:** Use `LOWER(name) GLOB` and make sure the search string is fully lowercase (for example, `LOWER(name) GLOB + '*renderthread*'`). Use this when dealing with inconsistent trace capitalization (for example, `WakeLock` versus `wakelock`). +- **Calculating Time Overlaps:** To calculate the overlap duration between two + time intervals `[start1, end1]` and `[start2, end2]`: + + > **Precedence Rule:** Always prefer using `SPAN_JOIN` or standard library + > functions (for example, `intervals.overlap`) to calculate overlaps + > **between two different sets of intervals** . Avoid manual arithmetic if a + > standard library feature or `SPAN_JOIN` can achieve the same result. + > Use the following logic if no built-in alternative exists. + 1. **Condition:** The intervals overlap if `start1 < end2` and `start2 < + end1`. + 2. **Duration:** The overlap duration is calculated as `MIN(end1, end2) - + MAX(start1, start2)` + + > **Important:** Incomplete Perfetto slices have a duration + > of -1 (`dur = -1`). Always calculate the effective end time using + > `ts + IIF(dur = -1, trace_end() - ts, dur)` before applying + > this logic. +- Query `android_thread_slices_for_all_startups` for app startup requests. + +- Join `counter_track` with `counter` to get values of counter with a specific + name. + +- When querying for a CPU frequency counter, include the `linux.cpu.frequency` + module and use the `cpu_frequency_counters` table. + +- When looking for events around a specific timestamp, start with 100ms as the + window size. + +- Always prefix column names with table or view alias, that is: + `{alias}.{column_name}`. + +- To calculate the total time spent in slices matching a specific name pattern + (for example, `*{name_pattern}*`), you must sum their durations. **Why it's + useful** : This helps quantify the total impact of a specific function or + feature on performance across multiple calls. Here is an example query + (note the safe handling of incomplete slices): + `sql + SELECT + count(*) as total_count, + sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / 1000000.0 as total_dur_ms + FROM slice + WHERE slice.name GLOB '*{name_pattern}*';` + +## Resources + +- **Documentation:** The Perfetto Standard Library documentation is in [`references/perfetto-stdlib-docs.md`](references/perfetto-stdlib-docs.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. + +## Execution Protocol + +You must follow these steps sequentially, mirroring a multi-agent pipeline: + +### Step 0: Tool Setup + +**Fetch the Wrapper:** If you don't have `trace_processor` in your current +working directory or in your path, download it directly from the Perfetto index: +`curl -LO https://get.perfetto.dev/trace_processor && chmod +x +./trace_processor` +> **Important:** The file served at this URL is a `~10KB` Python +> wrapper script. Don't assume the download failed because it is human-readable +> text. This is the intended behavior. This script handles lazy-loading the real +> binary into `~/.local/share/perfetto/` on its first run. Use it directly. + +### Step 1: Dissection and Schema Research + +1. Identify the core question, required data points, and filtering conditions. +2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](references/perfetto-stdlib-docs.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: + - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). + - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. + - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. + - **Targeted Bounded Reads:** Once you identify the relevant modules, efficiently read the tables and views within that module section. + - **Extract:** Extract only the schema, columns, and the exact `INCLUDE PERFETTO MODULE` statements for the required object from the documentation. + - **Verify:** Review the columns, types, and descriptions to ensure the table matches your needs. +4. Print the research results before drafting the query: +5. *Tables/Views:* `Schema for {name}:` listing columns and types. + +### Step 2: Draft and Validate Loop (Max 3 Iterations) + +Draft the SQL query in SQLite syntax using **only** the schemas +retrieved in Step 1. After drafting, you must validate against this checklist: + +- \[ \] **SQLite Syntax:** Does the query parse successfully without syntax errors? +- \[ \] **Idempotency:** Are all object creations safe to re-run? (Did you use `CREATE OR REPLACE PERFETTO` and `DROP TABLE IF EXISTS` for virtual tables?) +- \[ \] **Existence:** Were all tables found in the documentation? +- \[ \] **Intent Check:** Is there a pre-existing standard library table or view that will fulfill this intent before instead of writing manual arithmetic? +- \[ \] **Column Accuracy:** Do columns match the retrieved schemas? +- \[ \] **Alias Check:** Are ALL column names prefixed with their table or view alias (for example, `alias.column_name`)? +- \[ \] **Module Check:** Are `INCLUDE PERFETTO MODULE` statements included for all non-prelude modules? **You must use the exact module names provided in + the documentation.** +- \[ \] **Span Join Check:** If using `SPAN_JOIN`, are tables safely `PARTITIONED` to prevent overlapping interval crashes? Are intermediate tables materialized with `CREATE PERFETTO TABLE`? +- \[ \] **No LIKE Constraint:** Did you map string matches using `GLOB` or `=` instead of prohibited `LIKE`? +- \[ \] **Execution Check:** You MUST run queries using the standalone + `./trace_processor` wrapper with the `--query-string` flag: + `./trace_processor --query-string "QUERY" {trace_file}`. + + **Execution Rules:** + - **File Usage** : If you must create a SQL file to execute queries (for example, due to query length or escaping issues), you must create them in the `/tmp/` directory. + - **State:** The execution is purely ephemeral. Database state does not persist across turns. You **cannot** share state (like views or tables) across queries in different turns. Every query must be standalone and fully self-contained. + - **Failure Resilience:** Debug and fix SQL syntax and logic errors when query fails.Don't simplify the analytical intent to pass validation. For example, if requested to calculate an overlap or intersection, you must fix the intersection math. Don't substitute with disjoint queries (for example, returning independent total durations) as a workaround. + +### Step 3: Final Output + +1. Explicitly return and state the final validated SQL and explain the results to the user. +2. Before finishing your response, delete all temporary SQL files you created in `/tmp/` directory. diff --git a/profilers/perfetto-sql/references/perfetto-stdlib-docs.md b/profilers/perfetto-sql/references/perfetto-stdlib-docs.md new file mode 100644 index 0000000..ed4f4e9 --- /dev/null +++ b/profilers/perfetto-sql/references/perfetto-stdlib-docs.md @@ -0,0 +1,7044 @@ +*This page documents the PerfettoSQL standard library.* + +## Introduction + +The PerfettoSQL standard library is a repository of tables, views, functions +and macros, contributed by domain experts, which make querying traces easier. +Its design is heavily inspired by standard libraries in languages like Python, +C++ and Java. + +Some of the purposes of the standard library include: + +1. Acting as a way of sharing and commonly written queries without needing to copy/paste large amounts of SQL. +2. Raising the abstraction level when exposing data in the trace. Many modules in the standard library convert low-level trace concepts e.g. slices, tracks and into concepts developers may be more familar with e.g. for Android developers: app startups, binder transactions etc. + +Standard library modules can be included as follows: + +\`-- Include all tables/views/functions from the android.startup.startups +-- module in the standard library. +INCLUDE PERFETTO MODULE android.startup.startups; + +-- Use the android_startups table defined in the android.startup.startups +-- module. +SELECT \* +FROM android_startups;\`Prelude is a special module is automatically included. It contains key helper +tables, views and functions which are universally useful. + +More information on importing modules is available in the +[syntax documentation](https://perfetto.dev/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) +for the `INCLUDE PERFETTO MODULE` statement. + +## Package: prelude + +### prelude + +#### Views/Tables + +**perf_sample** + +VIEW +Samples from the traced_perf profiler. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this perf sample. | +| ts | TIMESTAMP | Timestamp of the sample. | +| utid | JOINID(thread.id) | Sampled thread. | +| cpu | LONG | Core the sampled thread was running on. | +| cpu_mode | STRING | Execution state (userspace/kernelspace) of the sampled thread. | +| callsite_id | JOINID(stack_profile_callsite.id) | If set, unwound callstack of the sampled thread. | +| unwind_error | STRING | If set, indicates that the unwinding for this sample encountered an error. Such samples still reference the best-effort result via the callsite_id, with a synthetic error frame at the point where unwinding stopped. | +| perf_session_id | JOINID(perf_session.id) | Distinguishes samples from different profiling streams (i.e. multiple data sources). | + +**counter** + +VIEW +Counters are values put into tracks during parsing of the trace. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique id of a counter value | +| ts | TIMESTAMP | Time of fetching the counter value. | +| track_id | JOINID(track.id) | Track this counter value belongs to. | +| value | DOUBLE | Value. | +| arg_set_id | ARGSETID | Additional information about the counter value. | + +**slice** + +VIEW +Contains slices from userspace which explains what threads were doing +during the trace. + +| Column | Type | Description | +|---|---|---| +| id | ID | The id of the slice. | +| ts | TIMESTAMP | The timestamp at the start of the slice in nanoseconds. The actual value depends on the `primary_trace_clock` selected in TraceConfig. This is often the value of a monotonic counter since device boot so is only meaningful in the context of a trace. | +| dur | DURATION | The duration of the slice in nanoseconds. | +| track_id | JOINID(track.id) | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | LONG | The depth of the slice in the current stack of slices. | +| parent_id | JOINID(slice.id) | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| arg_set_id | ARGSETID | The id of the argument set associated with this slice. | +| thread_ts | TIMESTAMP | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | DURATION | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| cat | STRING | Alias of `category`. | +| slice_id | JOINID(slice.id) | Alias of `id`. | + +**instant** + +VIEW +Contains instant events from userspace which indicates what happened at a +single moment in time. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | The timestamp of the instant. | +| track_id | JOINID(track.id) | The id of the track this instant is located on. | +| name | STRING | The name of the instant. The name describes what happened during the instant. | +| arg_set_id | ARGSETID | The id of the argument set associated with this instant. | + +**slices** + +VIEW +Alternative alias of table `slice`. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Alias of `slice.id`. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | +| thread_dur | DURATION | Alias of `slice.thread_dur`. | +| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | +| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | +| cat | STRING | Alias of `slice.cat`. | +| slice_id | JOINID(slice.id) | Alias of `slice.slice_id`. | + +**thread** + +VIEW +Contains information of threads seen during the trace. + +| Column | Type | Description | +|---|---|---| +| id | ID | The id of the thread. Prefer using `utid` instead. | +| utid | ID | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| tid | LONG | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use | +| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | TIMESTAMP | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | TIMESTAMP | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| upid | JOINID(process.id) | The process hosting this thread. | +| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | +| is_idle | BOOL | Boolean indicating if this thread is a kernel idle thread. | +| machine_id | LONG | Machine identifier, non-null for threads on a remote machine. | +| arg_set_id | ARGSETID | Extra args for this thread. | + +**process** + +VIEW +Contains information of processes seen during the trace. + +| Column | Type | Description | +|---|---|---| +| id | ID | The id of the process. Prefer using `upid` instead. | +| upid | JOINID(process.id) | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use | +| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | TIMESTAMP | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | TIMESTAMP | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| parent_upid | JOINID(process.id) | The upid of the process which caused this process to be spawned. | +| uid | LONG | The Unix user id of the process. | +| android_appid | LONG | Android appid of this process. | +| android_user_id | LONG | Android user id of this process. | +| cmdline | STRING | /proc/cmdline for this process. | +| arg_set_id | ARGSETID | Extra args for this process. | +| machine_id | LONG | Machine identifier, non-null for processes on a remote machine. | + +**args** + +VIEW +Arbitrary key-value pairs which allow adding metadata to other, strongly +typed tables. +Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| +will be non-null. + +| Column | Type | Description | +|---|---|---| +| id | ID | The id of the arg. | +| arg_set_id | ARGSETID | The id for a single set of arguments. | +| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | +| key | STRING | The key for the arg. | +| int_value | LONG | The integer value of the arg. | +| string_value | STRING | The string value of the arg. | +| real_value | DOUBLE | The double value of the arg. | +| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | +| display_value | STRING | The human-readable formatted value of the arg. | + +**perf_session** + +VIEW +Contains the Linux perf sessions in the trace. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the perf session. Prefer using `perf_session_id` instead. | +| perf_session_id | LONG | The id of the perf session. | +| cmdline | STRING | Command line used to collect the data. | + +**android_logs** + +VIEW +Log entries from Android logcat. + +NOTE: this table is not sorted by timestamp. + +| Column | Type | Description | +|---|---|---| +| id | ID | Which row in the table the log corresponds to. | +| ts | TIMESTAMP | Timestamp of log entry. | +| utid | JOINID(thread.id) | Thread writing the log entry. | +| prio | LONG | Priority of the log. 3=DEBUG, 4=INFO, 5=WARN, 6=ERROR. | +| tag | STRING | Tag of the log entry. | +| msg | STRING | Content of the log entry | + +**track** + +VIEW +Tracks are a fundamental concept in trace processor and represent a +"timeline" for events of the same type and with the same context. See + for a more +detailed explanation, with examples. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this track. Identical to | +| name | STRING | Name of the track; can be null for some types of tracks (e.g. thread tracks). | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given `type`. Join with the `args` table or use the `EXTRACT_ARG` helper function to expand the args. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Generic key-value pairs containing extra information about the track. Join with the `args` table or use the `EXTRACT_ARG` helper function to expand the args. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| track_group_id | LONG | An opaque key indicating that this track belongs to a group of tracks which are "conceptually" the same track. Tracks in trace processor don't allow overlapping events to allow for easy analysis (i.e. SQL window functions, SPAN JOIN and other similar operators). However, in visualization settings (e.g. the UI), the distinction doesn't matter and all tracks with the same `track_group_id` should be merged together into a single logical "UI track". | + +**thread_track** + +TABLE +Tracks which are associated to a single thread. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this thread track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| utid | JOINID(thread.id) | The utid that the track is associated with. | + +**process_track** + +TABLE +Tracks which are associated to a single process. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this process track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| upid | JOINID(process.id) | The upid that the track is associated with. | + +**cpu_track** + +TABLE +Tracks which are associated to a single CPU. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this cpu track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| cpu | LONG | The CPU that the track is associated with. | + +**gpu_track** + +TABLE +Table containing tracks which are loosely tied to a GPU. + +NOTE: this table is deprecated due to inconsistency of it's design with +other track tables (e.g. not having a GPU column, mixing a bunch of different +tracks which are barely related). Please use the track table directly +instead. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this cpu track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given type. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| scope | STRING | The source of the track. Deprecated. | +| description | STRING | The description for the track. | +| context_id | LONG | The context id for the GPU this track is associated to. | + +**ftrace_event** + +VIEW +Contains all the ftrace events in the trace. This table exists only for +debugging purposes and should not be relied on in production usecases (i.e. +metrics, standard library etc). Note also that this table might be empty if +raw ftrace parsing has been disabled. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this ftrace event. | +| ts | TIMESTAMP | The timestamp of this event. | +| name | STRING | The ftrace event name. | +| cpu | LONG | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | JOINID(thread.id) | The thread this event was emitted on. | +| arg_set_id | ARGSETID | The set of key/value pairs associated with this event. | +| common_flags | LONG | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | LONG | The unique CPU identifier that this event was emitted on. | + +**raw** + +VIEW +This table is deprecated. Use `ftrace_event` instead which contains the same +rows; this table is simply a (badly named) alias. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this raw event. | +| ts | TIMESTAMP | The timestamp of this event. | +| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | +| cpu | LONG | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | JOINID(thread.id) | The thread this event was emitted on. | +| arg_set_id | ARGSETID | The set of key/value pairs associated with this event. | +| common_flags | LONG | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | LONG | The unique CPU identifier that this event was emitted on. | + +**frame_slice** + +VIEW +Table containing graphics frame events on Android. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Alias of `slice.id`. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | LONG | Alias of `slice.arg_set_id`. | +| layer_name | STRING | Name of the graphics layer this slice happened on. | +| frame_number | LONG | The frame number this slice is associated with. | +| queue_to_acquire_time | LONG | The time between queue and acquire for this buffer and layer. | +| acquire_to_latch_time | LONG | The time between acquire and latch for this buffer and layer. | +| latch_to_present_time | LONG | The time between latch and present for this buffer and layer. | + +**gpu_slice** + +VIEW +Table containing graphics frame events on Android. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Alias of `slice.id`. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | LONG | Alias of `slice.arg_set_id`. | +| context_id | LONG | Context ID. | +| render_target | LONG | Render target ID. | +| render_target_name | STRING | The name of the render target. | +| render_pass | LONG | Render pass ID. | +| render_pass_name | STRING | The name of the render pass. | +| command_buffer | LONG | The command buffer ID. | +| command_buffer_name | STRING | The name of the command buffer. | +| frame_id | LONG | Frame id. | +| submission_id | LONG | The submission id. | +| hw_queue_id | LONG | The hardware queue id. | +| upid | JOINID(process.id) | The id of the process. | +| render_subpasses | STRING | Render subpasses. | + +**expected_frame_timeline_slice** + +TABLE +This table contains information on the expected timeline of either a display +frame or a surface frame. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Alias of `slice.id`. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | LONG | Alias of `slice.arg_set_id`. | +| display_frame_token | LONG | Display frame token (vsync id). | +| surface_frame_token | LONG | Surface frame token (vsync id), null if this is a display frame. | +| upid | JOINID(process.id) | Unique process id of the app that generates the surface frame. | +| layer_name | STRING | Layer name if this is a surface frame. | + +**actual_frame_timeline_slice** + +TABLE +This table contains information on the actual timeline and additional +analysis related to the performance of either a display frame or a surface +frame. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Alias of `slice.id`. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | LONG | Alias of `slice.arg_set_id`. | +| display_frame_token | LONG | Display frame token (vsync id). | +| upid | JOINID(process.id) | Surface frame token (vsync id), null if this is a display frame. | +| surface_frame_token | LONG | Unique process id of the app that generates the surface frame. | +| layer_name | STRING | Layer name if this is a surface frame. | +| present_type | STRING | Frame's present type (eg. on time / early / late). | +| on_time_finish | LONG | Whether the frame finishes on time. | +| gpu_composition | LONG | Whether the frame used gpu composition. | +| jank_type | STRING | Specify the jank types for this frame if there's jank, or none if no jank occurred. | +| jank_severity_type | STRING | Severity of the jank: none if no jank. | +| prediction_type | STRING | Frame's prediction type (eg. valid / expired). | +| jank_tag | STRING | Jank tag based on jank type, used for slice visualization. | +| jank_tag_experimental | STRING | Jank tag (experimental) based on jank type, used for slice visualization. | + +**heap_graph_class** + +VIEW +Stores class information within ART heap graphs. It represents Java/Kotlin +classes that exist in the heap, including their names, inheritance +relationships, and loading context. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this heap graph class. | +| name | STRING | (potentially obfuscated) name of the class. | +| deobfuscated_name | STRING | If class name was obfuscated and deobfuscation map for it provided, the deobfuscated name. | +| location | STRING | the APK / Dex / JAR file the class is contained in. | +| superclass_id | JOINID(heap_graph_class.id) | The superclass of this class. | +| classloader_id | LONG | The classloader that loaded this class. | +| kind | STRING | The kind of class. | + +**heap_graph_object** + +VIEW +The objects on the Dalvik heap. + +All rows with the same (upid, graph_sample_ts) are one dump. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this heap graph object. | +| upid | JOINID(process.id) | Unique PID of the target. | +| graph_sample_ts | TIMESTAMP | Timestamp this dump was taken at. | +| self_size | LONG | Size this object uses on the Java Heap. | +| native_size | LONG | Approximate amount of native memory used by this object, as reported by libcore.util.NativeAllocationRegistry.size. | +| reference_set_id | JOINID(heap_graph_reference.reference_set_id) | Join key with heap_graph_reference containing all objects referred in this object's fields. | +| reachable | BOOL | Bool whether this object is reachable from a GC root. If false, this object is uncollected garbage. | +| heap_type | STRING | The type of ART heap this object is stored on (app, zygote, boot image) | +| type_id | JOINID(heap_graph_class.id) | Class this object is an instance of. | +| root_type | STRING | If not NULL, this object is a GC root. | +| root_distance | LONG | Distance from the root object. | + +**heap_graph_reference** + +VIEW +Many-to-many mapping between heap_graph_object. + +This associates the object with given reference_set_id with the objects +that are referred to by its fields. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this heap graph reference. | +| reference_set_id | JOINID(heap_graph_object.reference_set_id) | Join key to heap_graph_object reference_set_id. | +| owner_id | JOINID(heap_graph_object.id) | Id of object that has this reference_set_id. | +| owned_id | JOINID(heap_graph_object.id) | Id of object that is referred to. | +| field_name | STRING | The field that refers to the object. E.g. Foo.name. | +| field_type_name | STRING | The static type of the field. E.g. java.lang.String. | +| deobfuscated_field_name | STRING | The deobfuscated name, if field_name was obfuscated and a deobfuscation mapping was provided for it. | + +**memory_snapshot** + +VIEW +Table with memory snapshots. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this snapshot. | +| timestamp | TIMESTAMP | Time of the snapshot. | +| track_id | JOINID(track.id) | Track of this snapshot. | +| detail_level | STRING | Detail level of this snapshot. | + +**process_memory_snapshot** + +VIEW +Table with process memory snapshots. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this snapshot. | +| snapshot_id | JOINID(memory_snapshot.id) | Snapshot ID for this snapshot. | +| upid | JOINID(process.id) | Process for this snapshot. | + +**memory_snapshot_node** + +VIEW +Table with memory snapshot nodes. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this node. | +| process_snapshot_id | JOINID(process_memory_snapshot.id) | Process snapshot ID for to this node. | +| parent_node_id | JOINID(memory_snapshot_node.id) | Parent node for this node, optional. | +| path | STRING | Path for this node. | +| size | LONG | Size of the memory allocated to this node. | +| effective_size | LONG | Effective size used by this node. | +| arg_set_id | ARGSETID | Additional args of the node. | + +**memory_snapshot_edge** + +VIEW + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this edge. | +| source_node_id | JOINID(memory_snapshot_node.id) | Source node for this edge. | +| target_node_id | JOINID(memory_snapshot_node.id) | Target node for this edge. | +| importance | LONG | Importance for this edge. | + +**counter_track** + +VIEW +Tracks containing counter-like events. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this cpu counter track. | +| name | STRING | Name of the track. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given type. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | + +**cpu_counter_track** + +TABLE +Tracks containing counter-like events associated to a CPU. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this cpu counter track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| cpu | LONG | The CPU that the track is associated with. | + +**gpu_counter_track** + +TABLE +Tracks containing counter-like events associated to a GPU. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this gpu counter track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| gpu_id | LONG | The GPU that the track is associated with. | + +**process_counter_track** + +TABLE +Tracks containing counter-like events associated to a process. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this process counter track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| upid | LONG | The upid of the process that the track is associated with. | + +**thread_counter_track** + +TABLE +Tracks containing counter-like events associated to a thread. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this thread counter track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | JOINID(track.id) | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| utid | LONG | The utid of the thread that the track is associated with. | + +**perf_counter_track** + +TABLE +Tracks containing counter-like events collected from Linux perf. + +| Column | Type | Description | +|---|---|---| +| id | ID(track.id) | Unique identifier for this thread counter track. | +| name | STRING | Name of the track. | +| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | +| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| perf_session_id | LONG | The id of the perf session this counter was captured on. | +| cpu | LONG | The CPU the counter is associated with. Can be null if the counter is not associated with any CPU. | +| is_timebase | BOOL | Whether this counter is the sampling timebase for the session. | + +**counters** + +VIEW +Alias of the `counter` table. + +| Column | Type | Description | +|---|---|---| +| id | ID | Alias of `counter.id`. | +| ts | TIMESTAMP | Alias of `counter.ts`. | +| track_id | JOINID(track.id) | Alias of `counter.track_id`. | +| value | DOUBLE | Alias of `counter.value`. | +| arg_set_id | LONG | Alias of `counter.arg_set_id`. | +| name | STRING | Legacy column, should no longer be used. | +| unit | STRING | Legacy column, should no longer be used. | + +**cpu** + +VIEW +Contains information about the CPUs on the device this trace was taken on. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this CPU. Identical to | +| ucpu | ID | Unique identifier for this CPU. Isn't equal to | +| cpu | LONG | The 0-based CPU core identifier. | +| cluster_id | LONG | The cluster id is shared by CPUs in the same cluster. | +| processor | STRING | A string describing this core. | +| machine_id | LONG | Machine identifier, non-null for CPUs on a remote machine. | +| capacity | LONG | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: | +| arg_set_id | ARGSETID | Extra key/value pairs associated with this cpu. | + +**cpu_available_frequencies** + +VIEW +Contains the frequency values that the CPUs on the device are capable of +running at. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this cpu frequency. | +| cpu | LONG | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| freq | LONG | CPU frequency in KHz. | +| ucpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | + +**sched** + +VIEW +Contains scheduling slices with kernel thread scheduling information. +These slices are collected when the Linux "ftrace" data source is used with +the "sched/switch" and "sched/wakeup\*" events enabled. + +The rows in this view will always have a matching row in the \|thread_state\| +table with \|thread_state.state\| = 'Running' + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this scheduling slice. | +| ts | TIMESTAMP | The timestamp at the start of the slice. | +| dur | DURATION | The duration of the slice. | +| cpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | JOINID(thread.id) | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | LONG | The kernel priority that the thread ran at. | +| ucpu | LONG | The unique CPU identifier that the slice executed on. | +| ts_end | LONG | Legacy column, should no longer be used. | + +**sched_slice** + +VIEW +Alias of `sched`. Prefer using `sched` instead. + +| Column | Type | Description | +|---|---|---| +| id | ID | Alias of `sched.id`. | +| ts | TIMESTAMP | Alias of `sched.ts`. | +| dur | DURATION | Alias of `sched.dur`. | +| cpu | LONG | Alias of `sched.cpu`. | +| utid | JOINID(thread.id) | Alias of `sched.utid`. | +| end_state | STRING | Alias of `sched.end_state`. | +| priority | LONG | Alias of `sched.priority`. | +| ucpu | LONG | Alias of `sched.ucpu`. | + +**thread_state** + +VIEW +This table contains the scheduling state of every thread on the system during +the trace. + +The rows in this table which have \|state\| = 'Running', will have a +corresponding row in the \|sched_slice\| table. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for this thread state. | +| ts | TIMESTAMP | The timestamp at the start of the slice. | +| dur | DURATION | The duration of the slice. | +| cpu | LONG | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | JOINID(thread.id) | The thread's unique id in the trace. | +| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in | +| io_wait | LONG | Indicates whether this thread was blocked on IO. | +| blocked_function | STRING | The function in the kernel this thread was blocked on. | +| waker_utid | JOINID(thread.id) | The unique thread id of the thread which caused a wakeup of this thread. | +| waker_id | JOINID(thread_state.id) | The unique thread state id which caused a wakeup of this thread. | +| irq_context | LONG | Whether the wakeup was from interrupt context or process context. | +| ucpu | LONG | The unique CPU identifier that the thread executed on. | + +**trace_metrics** + +VIEW +Lists all metrics built-into trace processor. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the metric. | + +**trace_bounds** + +VIEW +Definition of `trace_bounds` table. The values are being filled by Trace +Processor when parsing the trace. +It is recommended to depend on the `trace_start()` and `trace_end()` +functions rather than directly on `trace_bounds`. + +| Column | Type | Description | +|---|---|---| +| start_ts | TIMESTAMP | First ts in the trace. | +| end_ts | TIMESTAMP | End of the trace. | + +#### Functions + +**trace_start** + +Fetch start of the trace. +Returns TIMESTAMP: Start of the trace. + +**trace_end** + +Fetch end of the trace. +Returns TIMESTAMP: End of the trace. + +**trace_dur** + +Fetch duration of the trace. +Returns DURATION: Duration of the trace. + +**slice_is_ancestor** + +Given two slice ids, returns whether the first is an ancestor of the second. +Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. + +| Argument | Type | Description | +|---|---|---| +| ancestor_id | LONG | Id of the potential ancestor slice. | +| descendant_id | LONG | Id of the potential descendant slice. | + +#### Macros + +**cast_int** + +Casts \|value\| to INT. +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +**cast_double** + +Casts \|value\| to DOUBLE. +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +**cast_string** + +Casts \|value\| to STRING. +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +## Package: chrome + +### chrome.android_input + +#### Views/Tables + +**chrome_deliver_android_input_event** + +TABLE +DeliverInputEvent is the third step in the input pipeline. +It is responsible for routing the input events within browser process. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Touch move processing duration. | +| utid | LONG | Utid. | +| android_input_id | STRING | Input id (assigned by the system, used by InputReader and InputDispatcher) | + +**chrome_android_input** + +TABLE +Collects information about input reader, input dispatcher and +DeliverInputEvent steps for the given Android input id. + +| Column | Type | Description | +|---|---|---| +| android_input_id | STRING | Input id. | +| input_reader_processing_start_ts | TIMESTAMP | Input reader step start timestamp. | +| input_reader_processing_end_ts | TIMESTAMP | Input reader step end timestamp. | +| input_reader_utid | LONG | Input reader step utid. | +| input_dispatcher_processing_start_ts | TIMESTAMP | Input dispatcher step start timestamp. | +| input_dispatcher_processing_end_ts | TIMESTAMP | Input dispatcher step end timestamp. | +| input_dispatcher_utid | LONG | Input dispatcher step utid. | +| deliver_input_event_start_ts | TIMESTAMP | DeliverInputEvent step start timestamp. | +| deliver_input_event_end_ts | TIMESTAMP | DeliverInputEvent step end timestamp. | +| deliver_input_event_utid | LONG | DeliverInputEvent step utid. | + +### chrome.chrome_scrolls + +#### Views/Tables + +**chrome_scroll_update_refs** + +TABLE +Ties together input (`LatencyInfo.Flow`) and frame (`Graphics.Pipeline`) +trace events. Only covers input events of the `GESTURE_SCROLL_UPDATE_EVENT` +type. + +| Column | Type | Description | +|---|---|---| +| scroll_update_latency_id | LONG | Id of the Chrome input pipeline (`LatencyInfo.Flow`). | +| touch_move_latency_id | LONG | Id of the touch move input corresponding to this scroll update. | +| presentation_latency_id | LONG | Id of the `EventLatency` of the frame that the input was presented in. | +| surface_frame_id | LONG | Id of the frame pipeline (`Graphics.Pipeline`), pre-surface aggregation. | +| display_trace_id | LONG | Id of the frame pipeline (`Graphics.Pipeline`), post-surface aggregation. | + +**chrome_scroll_update_input_pipeline** + +TABLE +Timestamps and durations for the input-associated (before coalescing inputs +into a frame) stages of a scroll. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | +| scroll_id | LONG | Id of the scroll this scroll update belongs to. | +| presented_in_frame_id | LONG | Id of the frame that this input was presented in. Can be joined with `chrome_scroll_update_frame_pipeline.id`. | +| is_presented | BOOL | Whether this input event was presented. | +| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | +| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). If this is `true`, "generation" and "touch_move" related timestamps and durations will be null. | +| is_first_scroll_update_in_scroll | BOOL | Whether this is the first update in a scroll. First scroll update can never be janky. | +| is_first_scroll_update_in_frame | BOOL | Whether this is the first input that was presented in frame `presented_in_frame_id`. | +| generation_ts | TIMESTAMP | Input generation timestamp (from the Android system). | +| input_reader_processing_end_ts | TIMESTAMP | End timestamp for the InputReader step (see android_input.sql). Only populated when atrace 'input' category is enabled. | +| input_dispatcher_processing_end_ts | TIMESTAMP | End timestamp for the InputDispatcher step (see android_input.sql). Only populated when atrace 'input' category is enabled. | +| generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the input. | +| browser_utid | LONG | Utid for the browser main thread. | +| touch_move_received_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | +| touch_move_received_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | +| touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event. | +| scroll_update_created_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | +| scroll_update_created_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | +| scroll_update_processing_dur | DURATION | Duration for creating a `GestureScrollUpdate` from a `TouchMove` event. | +| scroll_update_created_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the above. | +| browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch. | +| compositor_utid | LONG | Utid for the renderer compositor thread. | +| compositor_dispatch_slice_id | LONG | Slice id for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | +| compositor_dispatch_ts | TIMESTAMP | Timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice or the containing task (if available). | +| compositor_dispatch_dur | DURATION | Duration for the compositor dispatch itself. | +| compositor_dispatch_end_ts | TIMESTAMP | End timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | +| compositor_dispatch_to_coalesced_input_handled_dur | DURATION | Duration between compositor dispatch and coalescing input. | +| compositor_coalesced_input_handled_slice_id | LONG | Slice id for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | +| compositor_coalesced_input_handled_ts | TIMESTAMP | Timestamp for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | +| compositor_coalesced_input_handled_dur | DURATION | Duration for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | +| compositor_coalesced_input_handled_end_ts | TIMESTAMP | End timestamp for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | + +**chrome_scroll_update_frame_pipeline** + +TABLE +Timestamps and durations for the frame-associated (after coalescing inputs +into a frame) stages of a scroll. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | +| display_trace_id | LONG | Id of the aggregated frame this scroll update was presented in. | +| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | +| compositor_resample_slice_id | LONG | Slice id for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | +| compositor_resample_ts | TIMESTAMP | Timestamp for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | +| compositor_receive_begin_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_BEGIN_FRAME` slice or the containing task (if available). | +| compositor_generate_compositor_frame_slice_id | LONG | Slice id for the `STEP_GENERATE_COMPOSITOR_FRAME` slice. | +| compositor_generate_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_GENERATE_COMPOSITOR_FRAME` slice or the containing task (if available). | +| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between generating and submitting the compositor frame. | +| compositor_submit_compositor_frame_slice_id | LONG | Slice id for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_submit_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_submit_frame_dur | DURATION | Duration for submitting the compositor frame (to viz). | +| compositor_submit_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | +| viz_compositor_utid | LONG | Utid for the viz compositor thread. | +| viz_receive_compositor_frame_slice_id | LONG | Slice id for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | +| viz_receive_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice or the containing task (if available). | +| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | +| viz_receive_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | +| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | +| viz_draw_and_swap_slice_id | LONG | Slice id for the `STEP_DRAW_AND_SWAP` slice. | +| viz_draw_and_swap_ts | TIMESTAMP | Timestamp for the `STEP_DRAW_AND_SWAP` slice or the containing task (if available). | +| viz_draw_and_swap_dur | DURATION | Duration for the viz drawing/swapping work for this frame. | +| viz_send_buffer_swap_slice_id | LONG | Slice id for the `STEP_SEND_BUFFER_SWAP` slice. | +| viz_send_buffer_swap_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_BUFFER_SWAP` slice. | +| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | +| viz_gpu_thread_utid | LONG | Utid for the viz `CompositorGpuThread`. | +| viz_swap_buffers_slice_id | LONG | Slice id for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | +| viz_swap_buffers_ts | TIMESTAMP | Timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice or the containing task (if available). | +| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | +| viz_swap_buffers_end_ts | TIMESTAMP | End timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | +| viz_swap_buffers_to_latch_dur | DURATION | Duration of `EventLatency`'s `BufferReadyToLatch` step. | +| latch_timestamp | TIMESTAMP | Timestamp for `EventLatency`'s `LatchToSwapEnd` step. | +| viz_latch_to_presentation_dur | DURATION | Duration of either `EventLatency`'s `LatchToSwapEnd` + `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation` step. | +| presentation_timestamp | TIMESTAMP | Presentation timestamp for the frame. | + +**chrome_scrolls** + +TABLE +Defines slices for all of the individual scrolls in a trace based on the +LatencyInfo-based scroll definition. + +NOTE: this view of top level scrolls is based on the LatencyInfo definition +of a scroll, which differs subtly from the definition based on +EventLatencies. +TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ +WebView instances. Currently gesture_scroll_id unique within an instance, but +is not unique across multiple instances. Switching to an EventLatency based +definition of scrolls should resolve this. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The unique identifier of the scroll. | +| ts | TIMESTAMP | The start timestamp of the scroll. | +| dur | DURATION | The duration of the scroll. | +| gesture_scroll_begin_ts | TIMESTAMP | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | +| gesture_scroll_end_ts | TIMESTAMP | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | + +**chrome_scroll_update_info** + +TABLE +Timestamps and durations for the critical path stages during scrolling. +This table covers both the input-associated (before coalescing inputs into a +frame) and frame-associated (after coalescing inputs into a frame) stages of +a scroll: + +`... +| ++---+---+ +| | +V V` +---+ +---+ +\| *scroll_update_INPUT* \| \| *scroll_update_FRAME* \| +\| timestamps_and_metadata \| \| timestamps_and_metadata \| ++---+---+ +---+---+ +\| \| +V V ++---+ +---+ +\| chrome_scroll*update* \| \| chrome_scroll*update* \| +\| INPUT_pipeline \| \| FRAME_pipeline \| ++---+---+ +---+---+ +\| \| ++---+---+ +\| +V ++---+ +\| chrome_scroll_update_info \| ++---+ + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | +| scroll_id | LONG | Id of the scroll this scroll update belongs to. | +| previous_input_id | LONG | Id (`LatencyInfo.ID`) of the previous input in this scroll. | +| frame_display_id | LONG | Id (`display_trace_id`) of the aggregated frame which this scroll update was presented in. | +| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | +| is_presented | BOOL | Whether this input event was presented. | +| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | +| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). If this is `true`, "generation" and "touch_move" related timestamps and durations will be null. | +| is_first_scroll_update_in_scroll | BOOL | Whether this is the first update in a scroll. First scroll update can never be janky. | +| is_first_scroll_update_in_frame | BOOL | Whether this is the first input that was presented in the frame. | +| browser_uptime_dur | DURATION | Duration from the start of the browser process to the first input generation timestamp. | +| generation_ts | TIMESTAMP | Input generation timestamp (from the Android system). | +| input_reader_dur | DURATION | Duration from the generation timestamp to the end of InputReader's work. Only populated when atrace 'input' category is enabled. | +| input_dispatcher_dur | DURATION | Duration of InputDispatcher's work. Only populated when atrace 'input' category is enabled. | +| since_previous_generation_dur | DURATION | Duration from the generation timestamp for the previous input to this input's generation timestamp. | +| generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the input. | +| browser_utid | LONG | Utid for the browser main thread. | +| touch_move_received_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | +| touch_move_received_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | +| touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event. | +| scroll_update_created_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | +| scroll_update_created_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | +| scroll_update_processing_dur | DURATION | Duration for creating a `GestureScrollUpdate` from a `TouchMove` event. | +| scroll_update_created_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the above. | +| browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch. | +| compositor_utid | LONG | Utid for the renderer compositor thread. | +| compositor_dispatch_slice_id | LONG | Slice id for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | +| compositor_dispatch_ts | TIMESTAMP | Timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice or the containing task (if available). | +| compositor_dispatch_dur | DURATION | Duration for the compositor dispatch itself. | +| compositor_dispatch_end_ts | TIMESTAMP | End timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | +| compositor_dispatch_to_on_begin_frame_delay_dur | DURATION | Duration between compositor dispatch and input resampling work. | +| compositor_resample_slice_id | LONG | Slice id for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | +| compositor_coalesced_input_handled_slice_id | LONG | Slice id for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | +| compositor_on_begin_frame_ts | TIMESTAMP | Start timestamp for work done on the input during "OnBeginFrame". | +| compositor_on_begin_frame_dur | DURATION | Duration of the "OnBeginFrame" work for this input. | +| compositor_on_begin_frame_end_ts | TIMESTAMP | End timestamp for work done on the input during "OnBeginFrame". | +| compositor_on_begin_frame_to_generation_delay_dur | DURATION | Delay until the compositor work for generating the frame begins. | +| compositor_generate_compositor_frame_slice_id | LONG | Slice id for the `STEP_GENERATE_COMPOSITOR_FRAME` slice. | +| compositor_generate_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_GENERATE_COMPOSITOR_FRAME` slice or the containing task (if available). | +| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between generating and submitting the compositor frame. | +| compositor_submit_compositor_frame_slice_id | LONG | Slice id for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_submit_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_submit_frame_dur | DURATION | Duration for submitting the compositor frame (to viz). | +| compositor_submit_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | +| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | +| viz_compositor_utid | LONG | Utid for the viz compositor thread. | +| viz_receive_compositor_frame_slice_id | LONG | Slice id for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | +| viz_receive_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice or the containing task (if available). | +| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | +| viz_receive_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | +| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | +| viz_draw_and_swap_slice_id | LONG | Slice id for the `STEP_DRAW_AND_SWAP` slice. | +| viz_draw_and_swap_ts | TIMESTAMP | Timestamp for the `STEP_DRAW_AND_SWAP` slice or the containing task (if available). | +| viz_draw_and_swap_dur | DURATION | Duration for the viz drawing/swapping work for this frame. | +| viz_send_buffer_swap_slice_id | LONG | Slice id for the `STEP_SEND_BUFFER_SWAP` slice. | +| viz_send_buffer_swap_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_BUFFER_SWAP` slice. | +| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | +| viz_gpu_thread_utid | LONG | Utid for the viz `CompositorGpuThread`. | +| viz_swap_buffers_slice_id | LONG | Slice id for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | +| viz_swap_buffers_ts | TIMESTAMP | Timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice or the containing task (if available). | +| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | +| viz_swap_buffers_end_ts | TIMESTAMP | End timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | +| viz_swap_buffers_to_latch_dur | DURATION | Duration of `EventLatency`'s `BufferReadyToLatch` step. | +| latch_timestamp | TIMESTAMP | Timestamp for `EventLatency`'s `LatchToSwapEnd` step. | +| viz_latch_to_presentation_dur | DURATION | Duration of either `EventLatency`'s `LatchToSwapEnd` + `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation` step. | +| presentation_timestamp | TIMESTAMP | Presentation timestamp for the frame. | + +**chrome_scroll_frame_info** + +TABLE +A list of all presented Chrome frames which contain scroll updates and associated +metadata. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id (frame's display_trace_id) for the given frame. | +| scroll_id | LONG | Id of the scroll this scroll update belongs to. | +| last_input_before_this_frame_id | LONG | Id (LatencyInfo.ID) of the last input before this frame. | +| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). TODO(b/394303662): Remove in favour of `vsync_interval_dur`. | +| vsync_interval_dur | DURATION | Vsync interval (in nanoseconds). | +| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | +| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). | +| total_input_delta_y | DOUBLE | Sum of all input deltas for all scroll updates in this frame. These values are based on the delta of the OS input events. | +| presented_scrolled_delta_y | DOUBLE | Presented delta (change in page offset) for the given frame. This delta is computed by Chrome (based on the input events). | +| browser_uptime_dur | DURATION | Duration from the start of the browser process to the first input generation timestamp. | +| first_input_generation_ts | TIMESTAMP | Input generation timestamp (from the Android system) for the first input. | +| input_reader_dur | DURATION | Duration from the generation timestamp to the end of InputReader's work. Only populated when atrace 'input' category is enabled. | +| input_dispatcher_dur | DURATION | Duration of InputDispatcher's work. Only populated when atrace 'input' category is enabled. | +| previous_last_input_to_first_input_generation_dur | DURATION | Duration from the previous input (last input that wasn't part of this frame) to the first input in this frame. | +| presentation_ts | TIMESTAMP | Presentation timestamp for the frame. | +| browser_utid | JOINID(thread.id) | Utid for the browser main thread. | +| first_input_generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the first input in this frame. | +| first_input_generation_to_browser_main_delta_dur | DURATION | Difference between `first_input_generation_to_browser_main_dur` for this frame and the previous frame in the same scroll. | +| first_input_touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event for the first input in this frame. | +| first_input_touch_move_processing_delta_dur | DURATION | Difference between `first_input_touch_move_processing_dur` for this frame and the previous frame in the same scroll. | +| compositor_utid | JOINID(thread.id) | Utid for the renderer compositor thread. | +| first_input_browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch for the first input in this frame. | +| first_input_browser_to_compositor_delay_delta_dur | DURATION | Difference between `first_input_browser_to_compositor_delay_dur` for this frame and the previous frame in the same scroll. | +| first_input_compositor_dispatch_dur | DURATION | Duration for the compositor dispatch for the first input in this frame. | +| first_input_compositor_dispatch_delta_dur | DURATION | Difference between `first_input_compositor_dispatch_dur` for this frame and the previous frame in the same scroll. | +| first_input_compositor_dispatch_to_on_begin_frame_delay_dur | DURATION | Duration between the compositor dispatch and the "OnBeginFrame" work for the first input in this frame. | +| first_input_compositor_dispatch_to_on_begin_frame_delay_delta_dur | DURATION | Difference between `first_input_compositor_dispatch_to_on_begin_frame_delay_dur` for this frame and the previous frame in the same scroll. | +| compositor_on_begin_frame_dur | DURATION | Duration of the "OnBeginFrame" work for this frame. | +| compositor_on_begin_frame_delta_dur | DURATION | Difference between `compositor_on_begin_frame_dur` for this frame and the previous frame in the same scroll. | +| compositor_on_begin_frame_to_generation_delay_dur | DURATION | Duration between the "OnBeginFrame" work and the generation of this frame. | +| compositor_on_begin_frame_to_generation_delay_delta_dur | DURATION | Difference between `compositor_on_begin_frame_to_generation_delay_dur` for this frame and the previous frame in the same scroll. | +| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between the generation and submission of this frame. | +| compositor_generate_frame_to_submit_frame_delta_dur | DURATION | Difference between `compositor_generate_frame_to_submit_frame_dur` for this frame and the previous frame in the same scroll. | +| compositor_submit_frame_dur | DURATION | Duration for submitting this frame. | +| compositor_submit_frame_delta_dur | DURATION | Difference between `compositor_submit_frame_dur` for this frame and the previous frame in the same scroll. | +| viz_compositor_utid | JOINID(thread.id) | Utid for the viz compositor thread. | +| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | +| compositor_to_viz_delay_delta_dur | DURATION | Difference between `compositor_to_viz_delay_dur` for this frame and the previous frame in the same scroll. | +| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | +| viz_receive_compositor_frame_delta_dur | DURATION | Difference between `viz_receive_compositor_frame_dur` for this frame and the previous frame in the same scroll. | +| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | +| viz_wait_for_draw_delta_dur | DURATION | Difference between `viz_wait_for_draw_dur` for this frame and the previous frame in the same scroll. | +| viz_draw_and_swap_dur | DURATION | Duration of the viz drawing/swapping work for this frame. | +| viz_draw_and_swap_delta_dur | DURATION | Difference between `viz_draw_and_swap_dur` for this frame and the previous frame in the same scroll. | +| viz_gpu_thread_utid | JOINID(thread.id) | Utid for the viz `CompositorGpuThread`. | +| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | +| viz_to_gpu_delay_delta_dur | DURATION | Difference between `viz_to_gpu_delay_dur` for this frame and the previous frame in the same scroll. | +| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | +| viz_swap_buffers_delta_dur | DURATION | Difference between `viz_swap_buffers_dur` for this frame and the previous frame in the same scroll. | +| viz_swap_buffers_to_latch_dur | DURATION | Time between buffers ready until Choreographer's latch. | +| viz_swap_buffers_to_latch_delta_dur | DURATION | Difference between `viz_swap_buffers_to_latch_dur` for this frame and the previous frame in the same scroll. | +| viz_latch_to_presentation_dur | DURATION | Duration between Choreographer's latch and presentation. | +| viz_latch_to_presentation_delta_dur | DURATION | Difference between `viz_latch_to_presentation_dur` for this frame and the previous frame in the same scroll. | + +**chrome_scroll_update_info_step_templates** + +TABLE +Source of truth for the definition of the stages of a scroll. Mainly intended +for visualization purposes (e.g. in Chrome Scroll Jank plugin). + +| Column | Type | Description | +|---|---|---| +| step_name | STRING | The name of a stage of a scroll. | +| ts_column_name | STRING | The name of the column in `chrome_scroll_update_info` which contains the timestamp of the stage. | +| dur_column_name | STRING | The name of the column in `chrome_scroll_update_info` which contains the duration of the stage. NULL if the stage doesn't have a duration. | + +### chrome.event_latency + +#### Views/Tables + +**chrome_event_latencies** + +TABLE +All EventLatency slices. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | TIMESTAMP | The start timestamp of the scroll. | +| dur | DURATION | The duration of the scroll. | +| scroll_update_id | LONG | The id of the scroll update event (aka LatencyInfo.ID). | +| surface_frame_trace_id | LONG | The id of the first frame (pre-surface aggregation) which included the scroll update and was presented. NULL if: (1) the event is not a scroll update (`event_type` is NOT GESTURE_SCROLL_UPDATE, FIRST_GESTURE_SCROLL_UPDATE, or INERTIAL_GESTURE_SCROLL_UPDATE), (2) the scroll update wasn't presented (e.g. it was an overscroll) or (3) the trace comes from an old Chrome version ( was first included in version 134.0.6977.0 and was cherry-picked in version 133.0.6943.33). | +| display_trace_id | LONG | The id of the first frame (post-surface aggregation) which included the scroll update and was presented. NULL if: (1) the event is not a scroll update (`event_type` is NOT GESTURE_SCROLL_UPDATE, FIRST_GESTURE_SCROLL_UPDATE, or INERTIAL_GESTURE_SCROLL_UPDATE), (2) the scroll update wasn't presented (e.g. it was an overscroll) or (3) the trace comes from an old Chrome version ( was first included in version 134.0.6977.0 and was cherry-picked in version 133.0.6943.33). | +| is_presented | BOOL | Whether this input event was presented. | +| event_type | STRING | EventLatency event type. | +| track_id | LONG | Perfetto track this slice is found on. | +| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | +| is_janky_scrolled_frame | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. | +| buffer_available_timestamp | LONG | Timestamp of the BufferAvailableToBufferReady substage. | +| buffer_ready_timestamp | LONG | Timestamp of the BufferReadyToLatch substage. | +| latch_timestamp | LONG | Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a fallback). | +| swap_end_timestamp | LONG | Timestamp of the SwapEndToPresentationCompositorFrame substage. | +| presentation_timestamp | LONG | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | + +**chrome_gesture_scroll_updates** + +TABLE +All scroll-related events (frames) including gesture scroll updates, begins +and ends with respective scroll ids and start/end timestamps, regardless of +being presented. This includes pinches that were presented. See b/315761896 +for context on pinches. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | TIMESTAMP | The start timestamp of the scroll. | +| dur | DURATION | The duration of the scroll. | +| scroll_update_id | LONG | The id of the scroll update event. | +| is_presented | BOOL | Whether this input event was presented. | +| event_type | STRING | EventLatency event type. | +| track_id | LONG | Perfetto track this slice is found on. | +| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | +| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. | +| buffer_available_timestamp | LONG | Timestamp of the BufferAvailableToBufferReady substage. | +| buffer_ready_timestamp | LONG | Timestamp of the BufferReadyToLatch substage. | +| latch_timestamp | LONG | Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a fallback). | +| swap_end_timestamp | LONG | Timestamp of the SwapEndToPresentationCompositorFrame substage. | +| presentation_timestamp | LONG | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | +| scroll_id | LONG | The id of the scroll. | + +### chrome.event_latency_description + +#### Views/Tables + +**chrome_event_latency_stage_descriptions** + +TABLE +Source of truth of the descriptions of EventLatency stages. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | A description of the EventLatency stage. | + +### chrome.graphics_pipeline + +#### Views/Tables + +**chrome_graphics_pipeline_surface_frame_steps** + +TABLE +`Graphics.Pipeline` steps corresponding to work done by a Viz client to +produce a frame (i.e. before surface aggregation). Covers steps: + +- STEP_ISSUE_BEGIN_FRAME +- STEP_RECEIVE_BEGIN_FRAME +- STEP_GENERATE_RENDER_PASS +- STEP_GENERATE_COMPOSITOR_FRAME +- STEP_SUBMIT_COMPOSITOR_FRAME +- STEP_RECEIVE_COMPOSITOR_FRAME +- STEP_RECEIVE_BEGIN_FRAME_DISCARD +- STEP_DID_NOT_PRODUCE_FRAME +- STEP_DID_NOT_PRODUCE_COMPOSITOR_FRAME + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice Id of the `Graphics.Pipeline` slice. | +| ts | TIMESTAMP | The start timestamp of the slice/step. | +| dur | DURATION | The duration of the slice/step. | +| step | STRING | Step name of the `Graphics.Pipeline` slice. | +| surface_frame_trace_id | LONG | Id of the graphics pipeline, pre-surface aggregation. | +| utid | LONG | Utid of the thread where this slice exists. | +| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | + +**chrome_graphics_pipeline_display_frame_steps** + +TABLE +`Graphics.Pipeline` steps corresponding to work done on creating and +presenting one frame during/after surface aggregation. Covers steps: + +- STEP_DRAW_AND_SWAP +- STEP_SURFACE_AGGREGATION +- STEP_SEND_BUFFER_SWAP +- STEP_BUFFER_SWAP_POST_SUBMIT +- STEP_FINISH_BUFFER_SWAP +- STEP_SWAP_BUFFERS_ACK + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice Id of the `Graphics.Pipeline` slice. | +| ts | TIMESTAMP | The start timestamp of the slice/step. | +| dur | DURATION | The duration of the slice/step. | +| step | STRING | Step name of the `Graphics.Pipeline` slice. | +| display_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | +| utid | LONG | Utid of the thread where this slice exists. | +| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | + +**chrome_surface_frame_id_to_first_display_id** + +TABLE +Links surface frames (`chrome_graphics_pipeline_surface_frame_steps`) to the +the first display frame (`chrome_graphics_pipeline_display_frame_steps`) into +which it was included. As an display frame usually aggregates frames from +multiple surfaces, multiple `surface_frame_trace_id`s will correspond to one +`display_trace_id`. + +| Column | Type | Description | +|---|---|---| +| surface_frame_trace_id | LONG | Id of the graphics pipeline, pre-surface aggregation. | +| display_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | + +**chrome_graphics_pipeline_inputs_to_surface_frames** + +TABLE +Links inputs (`chrome_input_pipeline_steps.latency_id`) to the surface frame +(`chrome_graphics_pipeline_surface_frame_steps`) to which they correspond. +In other words, in general, multiple `latency_id`s will correspond to one +`surface_frame_trace_id`. + +| Column | Type | Description | +|---|---|---| +| latency_id | LONG | Id corresponding to the input pipeline. | +| surface_frame_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | + +### chrome.histograms + +#### Views/Tables + +**chrome_histograms** + +TABLE +A helper view on top of the histogram events emitted by Chrome. +Requires "disabled-by-default-histogram_samples" Chrome category or the +"org.chromium.histogram_sample" data source. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the histogram. | +| value | LONG | The value of the histogram sample. | +| ts | TIMESTAMP | Alias of | +| thread_name | STRING | Thread name. | +| utid | LONG | Utid of the thread. | +| tid | LONG | Tid of the thread. | +| process_name | STRING | Process name. | +| upid | LONG | Upid of the process. | +| pid | LONG | Pid of the process. | + +### chrome.input + +#### Views/Tables + +**chrome_inputs** + +TABLE +Each row represents one input pipeline. + +| Column | Type | Description | +|---|---|---| +| latency_id | LONG | Id of this Chrome input pipeline (LatencyInfo). | +| input_type | STRING | Input type. | + +**chrome_input_pipeline_steps** + +TABLE +Since not all steps have associated input type (but all steps +for a given latency id should have the same input type), +populate input type for steps where it would be NULL. + +| Column | Type | Description | +|---|---|---| +| latency_id | LONG | Id of this Chrome input pipeline (LatencyInfo). | +| slice_id | LONG | Slice id | +| ts | TIMESTAMP | The step timestamp. | +| dur | DURATION | Step duration. | +| utid | LONG | Utid of the thread. | +| step | STRING | Step name (ChromeLatencyInfo.step). | +| input_type | STRING | Input type. | +| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | + +**chrome_coalesced_inputs** + +TABLE +For each input, if it was coalesced into another input, get the other input's +latency id. + +| Column | Type | Description | +|---|---|---| +| coalesced_latency_id | LONG | The `latency_id` of the coalesced input. | +| presented_latency_id | LONG | The `latency_id` of the other input that the current input was coalesced into. Guaranteed to be different from `coalesced_latency_id`. | + +**chrome_touch_move_to_scroll_update** + +TABLE +Each scroll update event (except flings) in Chrome starts its life as a touch +move event, which is then eventually converted to a scroll update itself. +Each of these events is represented by its own LatencyInfo. This table +contains a mapping between touch move events and scroll update events they +were converted into. + +| Column | Type | Description | +|---|---|---| +| touch_move_latency_id | LONG | Latency id of the touch move input (LatencyInfo). | +| scroll_update_latency_id | LONG | Latency id of the corresponding scroll update input (LatencyInfo). | + +**chrome_dispatch_android_input_event_to_touch_move** + +TABLE +Matches Android input id to the corresponding touch move event. + +| Column | Type | Description | +|---|---|---| +| android_input_id | STRING | Input id (assigned by the system, used by InputReader and InputDispatcher) | +| touch_move_latency_id | LONG | Latency id. | + +### chrome.interactions + +#### Views/Tables + +**chrome_interactions** + +TABLE +All critical user interaction events, including type and table with +associated metrics. + +| Column | Type | Description | +|---|---|---| +| scoped_id | LONG | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | +| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | +| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | +| ts | TIMESTAMP | Timestamp of the CUI event. | +| dur | DURATION | Duration of the CUI event. | + +### chrome.loadline_2 + +#### Views/Tables + +**chrome_loadline2_stages** + +TABLE + +| Column | Type | Description | +|---|---|---| +| page | STRING | Name of the page | +| story_start | TIMESTAMP | Story start timestamp | +| start_request | TIMESTAMP | Start request timestamp | +| end_request | TIMESTAMP | End request timestamp | +| renderer_ready | TIMESTAMP | Renderer ready timestamp | +| visual_mark | TIMESTAMP | Visual mark timestamp | +| visual_raf | TIMESTAMP | Visual rAF timestamp | +| visual_presentation | TIMESTAMP | Visual presentation timestamp | +| interactive_mark | TIMESTAMP | Interactive mark timestamp | +| interactive_raf | TIMESTAMP | Interactive rAF timestamp | +| interactive_presentation | TIMESTAMP | Interactive presentation timestamp | +| story_finish | TIMESTAMP | Story finish timestamp | + +### chrome.metadata + +#### Functions + +**chrome_hardware_class** + +Returns hardware class of the device, often use to find device brand +and model. +Returns STRING: Hardware class name. + +### chrome.page_loads + +#### Views/Tables + +**chrome_page_loads** + +TABLE +Chrome page loads, including associated high-level metrics and properties. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | +| navigation_id | LONG | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | +| navigation_start_ts | TIMESTAMP | Timestamp of the start of navigation. | +| fcp | LONG | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | +| fcp_ts | TIMESTAMP | Timestamp of the first contentful paint. | +| lcp | LONG | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | +| lcp_ts | TIMESTAMP | Timestamp of the largest contentful paint. | +| dom_content_loaded_event_ts | TIMESTAMP | Timestamp of the DomContentLoaded event: | +| load_event_ts | TIMESTAMP | Timestamp of the window load event: | +| mark_fully_loaded_ts | TIMESTAMP | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | +| mark_fully_visible_ts | TIMESTAMP | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | +| mark_interactive_ts | TIMESTAMP | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | +| url | STRING | URL at the page load event. | +| browser_upid | LONG | The unique process id (upid) of the browser process where the page load occurred. | + +### chrome.scroll_jank.predictor_error + +#### Views/Tables + +**chrome_predictor_error** + +TABLE +The scrolling offsets and predictor jank values for the actual (applied) +scroll events. + +| Column | Type | Description | +|---|---|---| +| scroll_id | LONG | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| scroll_update_id | LONG | An ID that ties this | +| present_ts | TIMESTAMP | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | +| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | +| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | +| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | +| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | +| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | + +### chrome.scroll_jank.scroll_jank_cause_map + +#### Views/Tables + +**chrome_scroll_jank_cause_descriptions** + +TABLE +Source of truth of the descriptions of EventLatency-based scroll jank causes. + +| Column | Type | Description | +|---|---|---| +| event_latency_stage | STRING | The name of the EventLatency stage. | +| cause_process | STRING | The process where the cause of scroll jank occurred. | +| cause_thread | STRING | The thread where the cause of scroll jank occurred. | +| cause_description | STRING | A description of the cause of scroll jank. | + +**chrome_scroll_jank_causes_with_event_latencies** + +VIEW +Combined description of scroll jank cause and associated event latency stage. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | Description of the EventLatency stage. | +| cause_process | STRING | The process name that may cause scroll jank. | +| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | +| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | + +### chrome.scroll_jank.scroll_jank_cause_utils + +#### Table Functions + +**chrome_select_scroll_jank_cause_thread** + +Function to retrieve the thread id of the thread on a particular process if +there are any slices during a particular EventLatency slice duration; this +upid/thread combination refers to a cause of Scroll Jank. + +| Argument | Type | Description | +|---|---|---| +| event_latency_id | LONG | The slice id of an EventLatency slice. | +| process_type | STRING | The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. | +| thread_name | STRING | The name of the thread. | + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | The utid associated with | + +### chrome.scroll_jank.scroll_jank_intervals + +#### Views/Tables + +**chrome_janky_event_latencies_v3** + +TABLE +Selects EventLatency slices that correspond with janks in a scroll. This is +based on the V3 version of scroll jank metrics. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The slice id. | +| ts | TIMESTAMP | The start timestamp of the slice. | +| dur | DURATION | The duration of the slice. | +| track_id | LONG | The track_id for the slice. | +| name | STRING | The name of the slice (EventLatency). | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| delayed_frame_count | LONG | How many vsyncs this frame missed its deadline by. | +| frame_jank_ts | TIMESTAMP | The start timestamp where frame presentation was delayed. | +| frame_jank_dur | LONG | The duration in ms of the delay in frame presentation. | + +**chrome_janky_frame_presentation_intervals** + +VIEW +Frame presentation interval is the delta between when the frame was supposed +to be presented and when it was actually presented. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique id. | +| ts | TIMESTAMP | The start timestamp of the slice. | +| dur | DURATION | The duration of the slice. | +| delayed_frame_count | LONG | How many vsyncs this frame missed its deadline by. | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| event_latency_id | LONG | The id of the associated event latency in the slice table. | + +**chrome_scroll_stats** + +TABLE +Scroll jank frame presentation stats for individual scrolls. + +| Column | Type | Description | +|---|---|---| +| scroll_id | LONG | Id of the individual scroll. | +| frame_count | LONG | The number of frames in the scroll. | +| missed_vsyncs | LONG | The number of missed vsyncs in the scroll. | +| presented_frame_count | LONG | The number presented frames in the scroll. | +| janky_frame_count | LONG | The number of janky frames in the scroll. | +| janky_frame_percent | DOUBLE | The % of frames that janked in the scroll. | + +**chrome_scroll_jank_intervals_v3** + +TABLE +Defines slices for all of janky scrolling intervals in a trace. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The unique identifier of the janky interval. | +| ts | TIMESTAMP | The start timestamp of the janky interval. | +| dur | DURATION | The duration of the janky interval. | + +### chrome.scroll_jank.scroll_jank_v3 + +#### Views/Tables + +**chrome_presented_gesture_scrolls** + +TABLE +Scroll updates, corresponding to all input events that were converted to a +presented scroll update. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Minimum slice id for input presented in this frame, the non-presented input. | +| ts | TIMESTAMP | The start timestamp for producing the frame. | +| dur | DURATION | The duration between producing and presenting the frame. | +| last_presented_input_ts | TIMESTAMP | The timestamp of the last input that arrived and got presented in the frame. | +| scroll_update_id | LONG | The id of the scroll update event, a unique identifier to the gesture. | +| scroll_id | LONG | The id of the ongoing scroll. | +| presentation_timestamp | LONG | Frame presentation timestamp. | +| event_type | STRING | EventLatency event type. | + +**chrome_scroll_updates_with_deltas** + +TABLE +Associate every trace_id with it's perceived delta_y on the screen after +prediction. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | LONG | The id of the scroll update event. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | + +**chrome_full_frame_view** + +TABLE +Obtain the subset of input events that were fully presented. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the frame. | +| ts | TIMESTAMP | Start timestamp of the frame. | +| last_presented_input_ts | TIMESTAMP | The timestamp of the last presented input. | +| scroll_id | LONG | ID of the associated scroll. | +| scroll_update_id | LONG | ID of the associated scroll update. | +| event_latency_id | LONG | ID of the associated EventLatency. | +| dur | DURATION | Duration of the associated EventLatency. | +| presentation_timestamp | LONG | Frame presentation timestamp. | + +**chrome_full_frame_delta_view** + +TABLE +Join deltas with EventLatency data. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the frame. | +| ts | TIMESTAMP | Start timestamp of the frame. | +| scroll_id | LONG | ID of the associated scroll. | +| scroll_update_id | LONG | ID of the associated scroll update. | +| last_presented_input_ts | TIMESTAMP | The timestamp of the last presented input. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | +| event_latency_id | LONG | ID of the associated EventLatency. | +| dur | DURATION | Duration of the associated EventLatency. | +| presentation_timestamp | LONG | Frame presentation timestamp. | + +**chrome_merged_frame_view** + +TABLE +Group all gestures presented at the same timestamp together in +a single row. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the frame. | +| max_start_ts | TIMESTAMP | The timestamp of the last presented input. | +| min_start_ts | TIMESTAMP | The earliest frame start timestamp. | +| scroll_id | LONG | ID of the associated scroll. | +| scroll_update_id | LONG | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | +| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | +| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | +| event_latency_id | LONG | ID of the associated EventLatency. | +| dur | DURATION | Maximum duration of the associated EventLatency. | +| presentation_timestamp | LONG | Frame presentation timestamp. | + +**chrome_frame_info_with_delay** + +TABLE +View contains all chrome presented frames during gesture updates +while calculating delay since last presented which usually should +equal to \|VSYNC_INTERVAL\| if no jank is present. + +| Column | Type | Description | +|---|---|---| +| id | LONG | gesture scroll slice id. | +| max_start_ts | TIMESTAMP | OS timestamp of the last touch move arrival within a frame. | +| min_start_ts | TIMESTAMP | OS timestamp of the first touch move arrival within a frame. | +| scroll_id | LONG | The scroll which the touch belongs to. | +| scroll_update_id | LONG | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | +| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | +| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives | +| event_latency_id | LONG | Event latency id of the presented frame. | +| dur | DURATION | Duration of the EventLatency. | +| presentation_timestamp | LONG | Timestamp at which the frame was shown on the screen. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals | +| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | +| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | + +**chrome_vsyncs** + +TABLE +Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the +minimum delay between frames larger than zero. + +TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +| Column | Type | Description | +|---|---|---| +| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | + +**chrome_janky_frames_no_cause** + +TABLE +Filter the frame view only to frames that had missed vsyncs. + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | +| event_latency_id | LONG | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | LONG | The scroll corresponding to this frame. | +| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | + +**chrome_janky_frames_no_subcause** + +TABLE +Janky frame information including the jank cause. + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | +| event_latency_id | LONG | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | LONG | The scroll corresponding to this frame. | +| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | +| cause_id | LONG | Id of the slice corresponding to the offending stage. | + +**chrome_janky_frames** + +TABLE +Finds all causes of jank for all janky frames, and a cause of sub jank +if the cause of jank was GPU related. + +| Column | Type | Description | +|---|---|---| +| cause_of_jank | STRING | The reason the Vsync was missed. | +| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | +| event_latency_id | LONG | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | LONG | The scroll corresponding to this frame. | + +**chrome_unique_frame_presentation_ts** + +TABLE +Counting all unique frame presentation timestamps. + +| Column | Type | Description | +|---|---|---| +| presentation_timestamp | LONG | The unique frame presentation timestamp. | + +**chrome_janky_frames_percentage** + +TABLE +Dividing missed frames over total frames to get janky frame percentage. +This represents the v3 scroll jank metrics. +Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +| Column | Type | Description | +|---|---|---| +| delayed_frame_percentage | DOUBLE | The percent of missed frames relative to total frames - aka the percent of janky frames. | + +**chrome_frames_per_scroll** + +TABLE +Number of frames and janky frames per scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_id | LONG | The ID of the scroll. | +| num_frames | LONG | The number of frames in the scroll. | +| num_janky_frames | LONG | The number of delayed/janky frames. | +| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | + +### chrome.scroll_jank.scroll_jank_v3_cause + +#### Functions + +**chrome_get_v3_jank_cause_id** + +Given two slice Ids A and B, find the maximum difference +between the durations of it's direct children with matching names +for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) +and slice B has children named (X, Y) with durations of (9, 9), the function will return +the slice id of the slice named Z that is A's child, as no matching slice named Z was found +under B, making 5 - 0 = 5 the maximum delta between both slice's direct children +Returns LONG: The slice id of the breakdown that has the maximum duration delta. + +| Argument | Type | Description | +|---|---|---| +| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | +| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to | + +### chrome.scroll_jank.scroll_offsets + +#### Views/Tables + +**chrome_scroll_input_deltas** + +TABLE +The raw input deltas for all input events which were part of a scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | LONG | Scroll update id (aka LatencyInfo.ID) for this scroll update input event. | +| delta_x | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the X axis vs previous, as reported by the OS. | +| delta_y | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the Y axis vs previous, as reported by the OS. | + +**chrome_scroll_input_offsets** + +TABLE +The raw coordinates and pixel offsets for all input events which were part of +a scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | LONG | An ID for this scroll update (aka LatencyInfo.ID). | +| scroll_id | LONG | An ID for the scroll this scroll update belongs to. | +| ts | TIMESTAMP | Timestamp the of the scroll input event. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The total delta of all scroll updates within the same as scroll up to and including this scroll update. | + +**chrome_scroll_presented_deltas** + +TABLE +The page offset delta (by how much the page was scrolled vs previous frame) +for each frame. +This is the resulting delta that is shown to the user after the input has +been processed. `chrome_scroll_input_deltas` tracks the underlying signal +deltas between consecutive input events. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | LONG | Scroll update id (aka LatencyInfo.ID) for this scroll update input event. | +| delta_x | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the X axis vs previous, as reported by the OS. | +| delta_y | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the Y axis vs previous, as reported by the OS. | +| offset_x | LONG | The page offset in pixels (scaled to the device's screen size) along the X axis. | +| offset_y | LONG | The page offset in pixels (scaled to the device's screen size) along the Y axis. | + +**chrome_presented_scroll_offsets** + +TABLE +The scrolling offsets for the actual (applied) scroll events. These are not +necessarily inclusive of all user scroll events, rather those scroll events +that are actually processed. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | LONG | An ID for this scroll update (aka LatencyInfo.ID). | +| scroll_id | LONG | An ID for the scroll this scroll update belongs to. | +| ts | TIMESTAMP | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +### chrome.scroll_jank.utils + +#### Table Functions + +**chrome_select_long_task_slices** + +Extract mojo information for the long-task-tracking scenario for specific +names. For example, LongTaskTracker slices may have associated IPC +metadata, or InterestingTask slices for input may have associated IPC to +determine whether the task is fling/etc. + +| Argument | Type | Description | +|---|---|---| +| name | STRING | The name of slice. | + +| Column | Type | Description | +|---|---|---| +| interface_name | STRING | Name of the interface of the IPC call. | +| ipc_hash | LONG | Hash of the IPC call. | +| message_type | STRING | Message type (e.g. reply). | +| id | LONG | The slice id. | + +### chrome.scroll_jank_tagging + +#### Views/Tables + +**chrome_scroll_jank_tags** + +TABLE +List of scroll jank causes that apply to janky scroll frames. +Each frame can have zero or multiple tags. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame ID. | +| tag | STRING | Tag of the scroll jank cause. | + +**chrome_tagged_janky_scroll_frames** + +TABLE +Consolidated list of tags for each janky scroll frame. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| tagged | BOOL | Whether this frame has any tags or not. | +| tags | STRING | Comma-separated list of tags for this frame. | + +### chrome.scroll_jank_v4 + +#### Views/Tables + +**chrome_scroll_jank_v4_results** + +TABLE +Results of the Scroll Jank V4 metric for frames which contain one or more +scroll updates. + +See + +and `EventLatency.ScrollJankV4Result` in + +for more information. + +Available since Chrome 145.0.7573.0 and cherry-picked into 144.0.7559.31. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Slice ID of the 'ScrollJankV4' slice. | +| name | STRING | Slice name ('ScrollJankV4'). | +| ts | TIMESTAMP | The timestamp at the start of the slice. | +| dur | DURATION | The duration of the slice. | +| is_janky | BOOL | Whether this frame is janky. True if and only if there's at least one row with `id` in `chrome_scroll_jank_v4_reasons`. If true, then `vsyncs_since_previous_frame` must be greater than one. | +| vsyncs_since_previous_frame | LONG | How many VSyncs were between (A) this frame and (B) the previous frame. If this value is greater than one, then Chrome potentially missed one or more VSyncs (i.e. might have been able to present this scroll update earlier). NULL if this frame is the first frame in a scroll. | +| running_delivery_cutoff | DURATION | The running delivery cut-off based on frames preceding this frame. NULL if ANY of the following holds: \* This frame is the first frame in a scroll. \* All frames since the beginning of the scroll up to and including the previous frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the previous frame have been non-damaging. | +| adjusted_delivery_cutoff | DURATION | The running delivery cut-off adjusted for this frame. NULL if ANY of the following holds: \* This frame is the first frame in a scroll. \* This frame is non-damaging. \* All frames since the beginning of the scroll up to and including the previous frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the previous frame have been non-damaging. \* `vsyncs_since_previous_frame` is equal to one. | +| current_delivery_cutoff | DURATION | The delivery cut-off of this frame. NULL if this frame is non-damaging. | +| real_first_event_latency_id | LONG | Trace ID of the first real scroll update included in this frame. Can be joined with `chrome_event_latencies.scroll_update_id`. NULL if this frame contains no real scroll updates. | +| real_first_input_generation_ts | TIMESTAMP | The actual generation timestamp of the first real scroll update included (coalesced) in this frame. NULL if this frame contains no real scroll updates. | +| real_last_input_generation_ts | TIMESTAMP | The actual generation timestamp of the last real scroll update included (coalesced) in this frame. NULL if this frame contains no real scroll updates. | +| real_abs_total_raw_delta_pixels | DOUBLE | The absolute total raw (unpredicted) delta of all real scroll updates included in this frame (in pixels). NULL if this frame contains no real scroll updates. | +| real_max_abs_inertial_raw_delta_pixels | DOUBLE | The maximum absolute raw (unpredicted) delta out of all inertial (fling) scroll updates included in this frame (in pixels). NULL if there were no inertial scroll updates in this frame. | +| synthetic_first_event_latency_id | LONG | Trace ID of the first synthetic scroll update included in this frame. Can be joined with `chrome_event_latencies.scroll_update_id`. NULL if this frame contains no synthetic scroll updates. | +| synthetic_first_extrapolated_input_generation_ts | TIMESTAMP | The generation timestamp of the first synthetic scroll update included (coalesced) in this frame extrapolated based on the input generation → begin frame duration of the most recent real scroll update. NULL if ANY of the following holds: \* This frame contains no synthetic scroll updates. \* This frame is janky (i.e. `is_janky` is true). \* All frames since the beginning of the scroll up to and including the previous frame have contained only synthetic scroll updates. \* The most recent janky frame contained only synthetic scroll updates and all frames since then up to and including the previous frame have contained only synthetic scroll updates. | +| synthetic_first_original_begin_frame_ts | TIMESTAMP | The begin frame timestamp of the first synthetic scroll update included (coalesced) in this frame. NULL if this frame contains no synthetic scroll updates. If not NULL, it's less than or equal to `begin_frame_ts`. | +| first_scroll_update_type | STRING | Type of the first scroll update in this frame. Possible values: \* 'REAL' \* 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' \* 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' The first scroll update is decided as follows: \* For real scroll updates, we consider their actual input generation timestamp. \* For synthetic scroll updates, we extrapolate their input generation timestamp based on the input generation → begin frame duration of the most recent real scroll update UNLESS ANY of the following holds (in which case we DON'T extrapolate input generation timestamps for synthetic scroll updates in this frame): \* This frame is janky. \* All frames since the beginning of the scroll up to and including the previous frame have contained only synthetic scroll updates. \* The most recent janky frame contained only synthetic scroll updates and all frames since then up to and including the previous frame have contained only synthetic scroll updates. If, based on the above rules, the scroll update with the earliest input generation timestamp is a real scroll update, then this frame's type is 'REAL'. If the scroll update with the earliest input generation timestamp is a synthetic scroll update, then this frame's type is 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP'. If this frame contains only synthetic scroll updates but it wasn't possible to extrapolate their input generation timestamp (for any of the reasons listed above), then this frame's type is 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP'. | +| first_event_latency_id | LONG | Trace ID of the first scroll update included in this frame. \* If `first_scroll_update_type` is 'REAL', then `first_event_latency_id` is equal to `real_first_event_latency_id`. \* If `first_scroll_update_type` is 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' or 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP', then `first_event_latency_id` equal to `synthetic_first_event_latency_id`. Can be joined with `chrome_event_latencies.scroll_update_id`. | +| damage_type | STRING | Type of scroll damage in this frame. Possible values: \* 'DAMAGING' \* 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP' \* 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP' A frame F is non-damaging if the following conditions are BOTH true: 1. All scroll updates in F are non-damaging. A scroll update is non-damaging if it didn't cause a frame update and/or didn't change the scroll offset. 2. All frames between (both ends exclusive): a. the last frame presented by Chrome before F and b. F are non-damaging. If this frame is damaging, its type is 'DAMAGING'. If this frame is non-damaging and its presentation timestamp could be extrapolated based on the begin frame → presentation duration of the most recent damaging frame, its type is 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP'. If this frame is non-damaging and its presentation timestamp couldn't be extrapolated for ANY of the reasons below, its type is 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP': \* This frame is janky and non-damaging. \* All frames since the beginning of the scroll up to and including this frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the this frame have been non-damaging. Note: The `first_scroll_update_type` and `damage_type` columns are orthogonal. The former depends on whether the frame is synthetic (only contains synthetic scroll updates). The latter depends on whether the frame is damaging. For example: \* If a frame is synthetic and damaging, it will\[1\] have an extrapolated input generation timestamp. \* If a frame is real and non-damaging, it will\[1\] have an extrapolated presentation timestamp. \* If a frame is both synthetic and damaging, it will\[1\] have both timestamps extrapolated. \[1\] As long as there's Chrome past performance to extrapolate based on. | +| vsync_interval | DURATION | The VSync interval that this frame was produced for according to the BeginFrameArgs. | +| begin_frame_ts | TIMESTAMP | The begin frame timestamp, at which this frame started, according to the BeginFrameArgs. | +| presentation_ts | TIMESTAMP | The presentation timestamp of the frame. \* If `damage_type` is 'DAMAGING', then `presentation_ts` is the actual presentation timestamp. \* If `damage_type` is 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP', then `presentation_ts` is an extrapolated timestamp based on the begin frame → presentation duration of the most recent damaging frame. \* If `damage_type` is 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP', then `presentation_ts` is NULL. | + +**chrome_scroll_jank_v4_reasons** + +TABLE +Reasons why the Scroll Jank V4 metric marked frames as janky. + +A frame might be janky for multiple reasons, so this table might contain +multiple rows with the same `id` and distinct `jank_reason`s. + +Available since Chrome 145.0.7573.0 and cherry-picked into 144.0.7559.31. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Slice ID of the 'ScrollJankV4' slice. Can be joined with `chrome_scroll_jank_v4_results.id`. | +| jank_reason | STRING | A reason why the frame is janky. Possible values: \* 'MISSED_VSYNC_DUE_TO_DECELERATING_INPUT_FRAME_DELIVERY': Chrome's input→frame delivery slowed down to the point that it missed one or more VSyncs. \* 'MISSED_VSYNC_DURING_FAST_SCROLL': Chrome missed one or more VSyncs in the middle of a fast regular scroll. \* 'MISSED_VSYNC_AT_START_OF_FLING': Chrome missed one or more VSyncs during the transition from a fast regular scroll to a fling. \* 'MISSED_VSYNC_DURING_FLING': Chrome missed one or more VSyncs in the middle of a fling. | +| missed_vsyncs | LONG | Number of VSyncs that that Chrome missed (for `jank_reason`) before presenting the first scroll update in the frame. Greater than zero. | + +### chrome.speedometer + +#### Views/Tables + +**chrome_speedometer_measure** + +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the measure slice | +| dur | DURATION | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | LONG | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +**chrome_speedometer_iteration** + +TABLE +Slice that covers one Speedometer iteration. +Depending on the Speedometer version these slices might need to be estimated +as older versions of Speedometer to not emit marks for this interval. The +metrics associated are the same ones Speedometer would output, but note we +use ns precision (Speedometer uses \~100us) so the actual values might differ +a bit. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the iteration | +| dur | DURATION | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | LONG | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +#### Functions + +**chrome_speedometer_score** + +Returns DOUBLE: Speedometer score + +**chrome_speedometer_renderer_main_utid** + +Returns LONG: Renderer main utid + +### chrome.speedometer_2_1 + +#### Views/Tables + +**chrome_speedometer_2_1_measure** + +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async +sync is the time between the start and sync-end marks, async is the time +between the sync-end and async-end marks. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the measure slice | +| dur | DURATION | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | LONG | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +**chrome_speedometer_2_1_iteration** + +TABLE +Slice that covers one Speedometer iteration. +This slice is actually estimated as a default Speedometer run will not emit +marks to cover this interval. The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. Also note Speedometer +returns the values in ms these here and in ns. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the iteration | +| dur | DURATION | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | LONG | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +#### Functions + +**chrome_speedometer_2_1_score** + +Returns the Speedometer 2.1 score for all iterations in the trace +Returns DOUBLE: Speedometer 2.1 score + +**chrome_speedometer_2_1_renderer_main_utid** + +Returns the utid for the main thread that ran Speedometer 2.1 +Returns LONG: Renderer main utid + +### chrome.speedometer_3 + +#### Views/Tables + +**chrome_speedometer_3_measure** + +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the measure slice | +| dur | DURATION | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | LONG | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +**chrome_speedometer_3_iteration** + +TABLE +Slice that covers one Speedometer iteration. +The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start timestamp of the iteration | +| dur | DURATION | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | LONG | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +#### Functions + +**chrome_speedometer_3_score** + +Returns DOUBLE: Speedometer 3 score + +**chrome_speedometer_3_renderer_main_utid** + +Returns LONG: Renderer main utid + +### chrome.startups + +#### Views/Tables + +**chrome_startups** + +TABLE +Chrome startups, including launch cause. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique ID | +| activity_id | LONG | Chrome Activity event id of the launch. | +| name | STRING | Name of the launch start event. | +| startup_begin_ts | TIMESTAMP | Timestamp that the startup occurred. | +| first_visible_content_ts | TIMESTAMP | Timestamp to the first visible content. | +| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | +| browser_upid | LONG | Process ID of the Browser where the startup occurred. | + +### chrome.tasks + +#### Views/Tables + +**chrome_java_views** + +VIEW +A list of slices corresponding to operations on interesting (non-generic) +Chrome Java views. The view is considered interested if it's not a system +(ContentFrameLayout) or generic library (CompositorViewHolder) views. + +TODO(altimin): Add "columns_from slice" annotation. +TODO(altimin): convert this to EXTEND_TABLE when it becomes available. + +| Column | Type | Description | +|---|---|---| +| filtered_name | STRING | Name of the view. | +| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | +| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | +| slice_id | LONG | Slice id. | + +**chrome_scheduler_tasks** + +VIEW +A list of tasks executed by Chrome scheduler. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice id. | +| type | STRING | Type. | +| name | STRING | Name of the task. | +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration. | +| utid | LONG | Utid of the thread this task run on. | +| thread_name | STRING | Name of the thread this task run on. | +| upid | LONG | Upid of the process of this task. | +| process_name | STRING | Name of the process of this task. | +| track_id | LONG | Same as slice.track_id. | +| category | STRING | Same as slice.category. | +| depth | LONG | Same as slice.depth. | +| parent_id | LONG | Same as slice.parent_id. | +| arg_set_id | LONG | Same as slice.arg_set_id. | +| thread_ts | TIMESTAMP | Same as slice.thread_ts. | +| thread_dur | DURATION | Same as slice.thread_dur. | +| posted_from | STRING | Source location where the PostTask was called. | + +**chrome_tasks** + +VIEW +A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / +IPCs / system callbacks) run by Chrome. For a given thread, the slices +corresponding to these tasks will not intersect. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id for the given task, also the id of the slice this task corresponds to. | +| name | STRING | Name for the given task. | +| task_type | STRING | Type of the task (e.g. "scheduler"). | +| thread_name | STRING | Thread name. | +| utid | LONG | Utid. | +| process_name | STRING | Process name. | +| upid | LONG | Upid. | +| ts | TIMESTAMP | Alias of | +| dur | DURATION | Alias of | +| track_id | LONG | Alias of | +| category | STRING | Alias of | +| arg_set_id | LONG | Alias of | +| thread_ts | TIMESTAMP | Alias of | +| thread_dur | DURATION | Alias of | +| full_name | STRING | STRING Legacy alias for | + +### chrome.vsync_intervals + +#### Views/Tables + +**chrome_vsync_intervals** + +TABLE +A simple table that checks the time between VSync (this can be used to +determine if we're refreshing at 90 FPS or 60 FPS). + +> [!NOTE] +> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +| Column | Type | Description | +|---|---|---| +| slice_id | LONG | Slice id of the vsync slice. | +| ts | TIMESTAMP | Timestamp of the vsync slice. | +| dur | DURATION | Duration of the vsync slice. | +| track_id | LONG | Track id of the vsync slice. | +| time_to_next_vsync | LONG | Duration until next vsync arrives. | + +#### Functions + +**chrome_calculate_avg_vsync_interval** + +Function: compute the average Vysnc interval of the +gesture (hopefully this would be either 60 FPS for the whole gesture or 90 +FPS but that isnt always the case) on the given time segment. +If the trace doesnt contain the VSync TraceEvent we just fall back on +assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which +corresponds to 16 ms or 60 FPS). +Returns DOUBLE: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. + +| Argument | Type | Description | +|---|---|---| +| begin_ts | TIMESTAMP | Interval start time. | +| end_ts | TIMESTAMP | Interval end time. | + +### chrome.web_content_interactions + +#### Views/Tables + +**chrome_web_content_interactions** + +TABLE +Chrome web content interactions (InteractionToFirstPaint), including +associated high-level metrics and properties. + +Multiple events may occur for the same interaction; each row in this table +represents the primary (longest) event for the interaction. + +Web content interactions are discrete, as opposed to sustained (e.g. +scrolling); and only occur with the web content itself, as opposed to other +parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, +keyboard input (typing), and drags. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique id for this interaction. | +| ts | TIMESTAMP | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | +| dur | DURATION | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | +| interaction_type | STRING | The interaction type. | +| total_duration_ms | LONG | The total duration of all events that occurred for the same interaction. | +| renderer_upid | LONG | The process id this event occurred on. | + +## Package: android + +### android.anrs + +#### Views/Tables + +**android_anrs** + +TABLE +List of all ANRs that occurred in the trace (one row per ANR). + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that triggered the ANR. | +| pid | LONG | PID of the process that triggered the ANR. | +| upid | JOINID(process.id) | UPID of the process that triggered the ANR. | +| error_id | STRING | UUID of the ANR (generated on the platform). | +| ts | TIMESTAMP | Timestamp of the ANR. | +| subject | STRING | Subject line of the ANR. | +| intent | STRING | The intent that caused the ANR (if applicable). | +| component | STRING | The component associated with the ANR (if applicable). | +| timer_delay | LONG | The duration between the timer expiration event and the anr counter event | +| anr_type | STRING | The standard type of ANR. | +| anr_dur_ms | LONG | Duration of the ANR, computed from the timer expiration event. | +| default_anr_dur_ms | LONG | Default duration of the ANR, based on the anr_type (default means in AOSP/Pixel). Note: Other OEMs may have customized these timeout values, so the defaults provided here might not be accurate for all devices. | + +### android.app_process_starts + +#### Views/Tables + +**android_app_process_starts** + +TABLE +All app cold starts with information about their cold start reason: +broadcast, service, activity or provider. + +| Column | Type | Description | +|---|---|---| +| start_id | LONG | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | +| id | LONG | Slice id of intent received in the app. | +| track_id | JOINID(track.id) | Track id of the intent received in the app. | +| process_name | STRING | Name of the process receiving the intent. | +| pid | LONG | Pid of the process receiving the intent. | +| upid | JOINID(process.id) | Upid of the process receiving the intent. | +| intent | STRING | Intent action or component responsible for the cold start. | +| reason | STRING | Process start reason: activity, broadcast, service or provider. | +| proc_start_ts | TIMESTAMP | Timestamp the process start was dispatched from system_server. | +| proc_start_dur | DURATION | Duration to dispatch the process start from system_server. | +| bind_app_ts | TIMESTAMP | Timestamp the bindApplication started in the app. | +| bind_app_dur | DURATION | Duration to complete bindApplication in the app. | +| intent_ts | TIMESTAMP | Timestamp the Intent was received in the app. | +| intent_dur | DURATION | Duration to handle intent in the app. | +| total_dur | LONG | Total duration from proc_start dispatched to intent completed. | + +### android.auto.multiuser + +#### Views/Tables + +**android_auto_multiuser_timing** + +TABLE +Time elapsed between the latest user start +and the specific end event +like package startup(ex carlauncher) or previous user stop. + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Id of the started android user | +| event_start_time | LONG | Start event time | +| event_end_time | LONG | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | + +**android_auto_multiuser_timing_with_previous_user_resource_usage** + +VIEW +This table extends `android_auto_multiuser_timing` table with previous user resource usage. + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Start user id | +| event_start_time | LONG | Start event time | +| event_end_time | LONG | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | +| user_id | LONG | User id | +| total_cpu_time | LONG | Total CPU time for a user | +| total_memory_usage_kb | LONG | Total memory user for a user | + +### android.battery + +#### Views/Tables + +**android_battery_charge** + +VIEW +Battery charge at timestamp. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| current_avg_ua | DOUBLE | Current average micro ampers. | +| capacity_percent | DOUBLE | Current capacity percentage. | +| charge_uah | DOUBLE | Current charge in micro ampers. | +| current_ua | DOUBLE | Current micro ampers. | +| voltage_uv | DOUBLE | Current voltage in micro volts. | +| energy_counter_uwh | DOUBLE | Current energy counter in microwatt-hours(µWh). | +| power_mw | DOUBLE | Current power in milliwatts. | + +### android.battery.charging_states + +#### Views/Tables + +**android_charging_states** + +TABLE +Device charging states. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Alias of counter.id if a slice with charging state exists otherwise there will be a single row where id = 1. | +| ts | TIMESTAMP | Timestamp at which the device charging state began. | +| dur | DURATION | Duration of the device charging state. | +| short_charging_state | STRING | One of: charging, discharging, not_charging, full, unknown. | +| charging_state | STRING | Device charging state, one of: Charging, Discharging, Not charging (when the charger is present but battery is not charging), Full, Unknown | + +### android.battery.doze + +#### Views/Tables + +**android_light_idle_state** + +TABLE +Light idle states. This is the state machine that quickly detects the +device is unused and restricts background activity. +See + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID | +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration. | +| light_idle_state | STRING | Description of the light idle state. | + +**android_deep_idle_state** + +TABLE +Deep idle states. This is the state machine that more slowly detects deeper +levels of device unuse and restricts background activity further. +See + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID | +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration. | +| deep_idle_state | STRING | Description of the deep idle state. | + +### android.battery_stats + +#### Views/Tables + +**android_battery_stats_state** + +VIEW +View of human readable battery stats counter-based states. These are recorded +by BatteryStats as a bitmap where each 'category' has a unique value at any +given time. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the new barrary state. | +| dur | DURATION | The duration the state was active, -1 for incomplete slices. | +| safe_dur | DURATION | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| value | LONG | The counter value as a number. | +| value_name | STRING | The counter value as a human-readable string. | + +**android_battery_stats_event_slices** + +VIEW +View of slices derived from battery_stats events. Battery stats records all +events as instants, however some may indicate whether something started or +stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground +apps or long wakes include these details and allow drawing slices between +instant events found in a trace. + +For example, we may see an event like the following on 'battery_stats.top': + +`-top=10215:"com.google.android.apps.nexuslauncher"` This view will find the associated start ('+top') with the matching suffix +(everything after the '=') to construct a slice. It computes the timestamp +and duration from the events and extract the details as follows: + +`track_name='battery_stats.top' +str_value='com.google.android.apps.nexuslauncher' +int_value=10215` + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of a new battery state. | +| dur | DURATION | The duration the state was active, -1 for incomplete slices. | +| safe_dur | DURATION | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| str_value | STRING | String value. | +| int_value | LONG | Int value. | + +#### Functions + +**android_battery_stats_counter_to_string** + +Converts a battery_stats counter value to human readable string. +Returns STRING: The human-readable name for the counter value. + +| Argument | Type | Description | +|---|---|---| +| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | +| value | DOUBLE | The counter value. | + +### android.binder + +#### Views/Tables + +**android_binder_metrics_by_process** + +VIEW +Count Binder transactions per process. + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that started the binder transaction. | +| pid | LONG | PID of the process that started the binder transaction. | +| slice_name | STRING | Name of the slice with binder transaction. | +| event_count | LONG | Number of binder transactions in process in slice. | + +**android_sync_binder_thread_state_by_txn** + +VIEW +Aggregated thread_states on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the thread state durations. +The \|thread_state_type\| column represents whether a given 'aggregated thread_state' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | LONG | slice id of the binder txn | +| client_ts | TIMESTAMP | Client timestamp | +| client_tid | LONG | Client tid | +| binder_reply_id | LONG | slice id of the binder reply | +| server_ts | TIMESTAMP | Server timestamp | +| server_tid | LONG | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| thread_state | STRING | a thread_state that occurred in the txn | +| thread_state_dur | DURATION | aggregated dur of the | +| thread_state_count | LONG | aggregated count of the | + +**android_sync_binder_blocked_functions_by_txn** + +VIEW +Aggregated blocked_functions on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the kernel blocked function durations. +The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | LONG | slice id of the binder txn | +| client_ts | TIMESTAMP | Client ts | +| client_tid | LONG | Client tid | +| binder_reply_id | LONG | slice id of the binder reply | +| server_ts | TIMESTAMP | Server ts | +| server_tid | LONG | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| blocked_function | STRING | blocked kernel function in a thread state | +| blocked_function_dur | DURATION | aggregated dur of the | +| blocked_function_count | LONG | aggregated count of the | + +**android_binder_txns** + +TABLE +Breakdown binder transactions per txn. +It returns data about the client and server ends of every binder transaction async. + +| Column | Type | Description | +|---|---|---| +| aidl_name | STRING | Fully qualified name of the binder endpoint if existing. | +| interface | STRING | Interface of the binder endpoint if existing. | +| method_name | STRING | Method name of the binder endpoint if existing. | +| aidl_ts | TIMESTAMP | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | +| aidl_dur | DURATION | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | +| binder_txn_id | JOINID(slice.id) | Slice id of the binder txn. | +| client_process | STRING | Name of the client process. | +| client_thread | STRING | Name of the client thread. | +| client_upid | JOINID(process.id) | Upid of the client process. | +| client_utid | JOINID(thread.id) | Utid of the client thread. | +| client_tid | LONG | Tid of the client thread. | +| client_pid | LONG | Pid of the client thread. | +| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | +| client_ts | TIMESTAMP | Timestamp of the client txn. | +| client_dur | DURATION | Wall clock dur of the client txn. | +| binder_reply_id | JOINID(slice.id) | Slice id of the binder reply. | +| server_process | STRING | Name of the server process. | +| server_thread | STRING | Name of the server thread. | +| server_upid | JOINID(process.id) | Upid of the server process. | +| server_utid | JOINID(thread.id) | Utid of the server thread. | +| server_tid | LONG | Tid of the server thread. | +| server_pid | LONG | Pid of the server thread. | +| server_ts | TIMESTAMP | Timestamp of the server txn. | +| server_dur | DURATION | Wall clock dur of the server txn. | +| client_oom_score | LONG | Oom score of the client process at the start of the txn. | +| server_oom_score | LONG | Oom score of the server process at the start of the reply. | +| is_sync | BOOL | Whether the txn is synchronous or async (oneway). | +| client_monotonic_dur | DURATION | Monotonic clock dur of the client txn. | +| server_monotonic_dur | DURATION | Monotonic clock dur of the server txn. | +| client_package_version_code | LONG | Client package version_code. | +| server_package_version_code | LONG | Server package version_code. | +| is_client_package_debuggable | BOOL | Whether client package is debuggable. | +| is_server_package_debuggable | BOOL | Whether server package is debuggable. | + +#### Table Functions + +**android_binder_outgoing_graph** + +Returns a DAG of all outgoing binder txns from a process. +The roots of the graph are the threads making the txns and the graph flows from: +thread -\> server_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. + +| Argument | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of process to generate an outgoing graph for. | + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of outgoing binder txns. | + +**android_binder_incoming_graph** + +Returns a DAG of all incoming binder txns from a process. +The roots of the graph are the clients making the txns and the graph flows from: +client_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. + +| Argument | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of process to generate an incoming graph for. | + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of incoming binder txns. | + +**android_binder_graph** + +Returns a graph of all binder txns in a trace. +The nodes are client_process and server_process. +The weights of each node represent the wall execution time in the server_process. + +| Argument | Type | Description | +|---|---|---| +| min_client_oom_score | LONG | Matches txns from client_processes greater than or equal to the OOM score. | +| max_client_oom_score | LONG | Matches txns from client_processes less than or equal to the OOM score. | +| min_server_oom_score | LONG | Matches txns to server_processes greater than or equal to the OOM score. | +| max_server_oom_score | LONG | Matches txns to server_processes less than or equal to the OOM score. | + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of binder txns. | + +### android.binder_breakdown + +#### Views/Tables + +**android_binder_server_breakdown** + +TABLE +Server side binder breakdowns per transactions per txn. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | +| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | +| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder reply with a single reason. | +| dur | DURATION | Duration of an exclusive interval during the binder reply with a single reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | + +**android_binder_client_breakdown** + +TABLE +Client side binder breakdowns per transactions per txn. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | +| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | +| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder txn with a single latency reason. | +| dur | DURATION | Duration of an exclusive interval during the binder txn with a single latency reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | + +**android_binder_client_server_breakdown** + +TABLE +Combined client and server side binder breakdowns per transaction. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | +| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | +| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder txn with a single latency reason. | +| dur | DURATION | Duration of an exclusive interval during the binder txn with a single latency reason. | +| server_reason | STRING | The server side component of this interval's binder latency reason, if any. | +| client_reason | STRING | The client side component of this interval's binder latency reason. | +| reason | STRING | Combined reason indicating whether latency came from client or server side. | +| reason_type | STRING | Whether the latency is due to the client or server. | + +### android.bitmaps + +#### Views/Tables + +**android_bitmap_memory** + +TABLE +Provides a timeseries of "Bitmap Memory" counter for each process, which +is useful for retrieving the total memory used by bitmaps by an application over time. + +To populate this table, tracing must be enabled with the "view" atrace +category. + +| Column | Type | Description | +|---|---|---| +| id | ID(counter.id) | ID of the row in the underlying counter table. | +| upid | JOINID(process.upid) | Upid of the process. | +| ts | TIMESTAMP | Timestamp of the start of the interval. | +| dur | DURATION | Duration of the interval. | +| track_id | JOINID(counter.track_id) | Duration of the interval. | +| value | LONG | Memory consumed by bitmaps in bytes. | + +**android_bitmap_count** + +TABLE +Provides a timeseries of "Bitmap Count" counter for each process, which +is useful for retrieving the number of bitmaps used by an application over time. + +To populate this table, tracing must be enabled with the "view" atrace +category. + +| Column | Type | Description | +|---|---|---| +| id | ID(counter.id) | ID of the row in the underlying counter table. | +| upid | JOINID(process.upid) | Upid of the process. | +| ts | TIMESTAMP | Timestamp of the start of the interval. | +| dur | DURATION | Duration of the interval. | +| track_id | JOINID(counter.track_id) | Duration of the interval. | +| value | LONG | Number of allocated bitmaps. | + +**android_bitmap_counters_per_process** + +TABLE +Provides a timeseries of bitmap-related counters for each process, which +is useful for understanding an application's bitmap usage over time. + +To populate this table, tracing must be enabled with the "view" atrace +category. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.upid) | Upid of the process. | +| process_name | STRING | Name of the process. | +| ts | TIMESTAMP | Timestamp of the start of the interval. | +| dur | DURATION | Duration of the interval. | +| bitmap_memory | LONG | Memory consumed by bitmaps in bytes. | +| bitmap_count | LONG | Number of allocated bitmaps. | +| bitmap_memory_id | JOINID(counter.id) | ID of the row in the underlying counter table. | +| bitmap_count_id | JOINID(counter.id) | ID of the row in the underlying counter table. | + +### android.cpu.cluster_type + +#### Views/Tables + +**android_cpu_cluster_mapping** + +TABLE +Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. +This cluster type is determined by initially using cpu_capacity from sysfs +and grouping clusters with identical capacities, ordered by size. +In the case that capacities are not present, max frequency is used instead. +If nothing is avaiable, NULL is returned. + +| Column | Type | Description | +|---|---|---| +| ucpu | LONG | Alias of `cpu.ucpu`. | +| cpu | LONG | Alias of `cpu.cpu`. | +| cluster_type | STRING | The cluster type of the CPU. | + +### android.cpu.cpu_per_uid + +#### Views/Tables + +**android_cpu_per_uid_track** + +TABLE +Table of tracks for CPU-per-UID data. Each row represents one UID / cluster +combination. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the track; can be joined with cpu_per_uid_counter. | +| uid | LONG | UID doing the work. | +| cluster | LONG | Cluster ID for the track, starting from 0, typically with larger numbers meaning larger cores. | +| total_cpu_millis | LONG | Total number of cpu millis used by this track. | +| package_name | STRING | A package name for the UID. If there are multiple for a UID, one is chosen arbitrarily. UIDs below 10000 always have null package name. | + +**android_cpu_per_uid_counter** + +VIEW +View of counters for CPU-per-UID data. Each row represents one instant in +time for one UID / cluster. + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID for the row. | +| ts | LONG | Timestamp for the row. | +| dur | LONG | Time to the next measurement for the UID / cluster combination. | +| track_id | LONG | Associated track. | +| diff_ms | LONG | CPU time measurement for this time period (milliseconds). | +| cpu_ratio | DOUBLE | Inferred CPU use value for the period where 1.0 means a single core at 100% utilisation. | + +### android.cujs.base + +#### Views/Tables + +**android_jank_cuj** + +TABLE +Table tracking all jank CUJs information. + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | +| cuj_slice_name | STRING | Name of the CUJ slice. | +| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | +| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | +| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | +| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | +| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | +| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | +| ui_thread | JOINID(thread.id) | thread id of the UI thread. | +| layer_id | LONG | layer id associated with the actual frame. | +| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | +| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | + +### android.cujs.sysui_cujs + +#### Views/Tables + +**android_sysui_jank_cujs** + +TABLE +Table tracking all jank CUJs information. + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | +| cuj_slice_name | STRING | Name of the CUJ slice. | +| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | +| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | +| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | +| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | +| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | +| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | +| ui_thread | JOINID(thread.id) | thread id of the UI thread. | +| layer_id | LONG | layer id associated with the actual frame. | +| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | +| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | + +**android_sysui_latency_cujs** + +TABLE +Table tracking all latency CUJs information. + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | +| cuj_slice_name | STRING | Name of the CUJ slice. | +| cuj_name | STRING | Name of the CUJ without the 'L\<' prefix. | +| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | +| ts | TIMESTAMP | Start timestamp of the CUJ calculated as the start of the CUJ slice in trace. | +| ts_end | TIMESTAMP | End timestamp of the CUJ calculated as the end timestamp of the CUJ slice. | +| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | +| state | STRING | State of the CUJ whether it was completed/cancelled. | + +**android_jank_latency_cujs** + +TABLE +Table tracking all jank/latency CUJs information. + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| id | LONG | An alias for cuj_id for compatibility purposes. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | +| cuj_slice_name | STRING | Name of the CUJ slice. | +| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | +| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | +| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | +| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | +| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | +| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | +| ui_thread | JOINID(thread.id) | thread id of the UI thread. In case of latency CUJs, this will always be the main thread of the process. | +| layer_id | LONG | layer id associated with the actual frame. | +| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | +| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | +| cuj_type | STRING | Type of CUJ, i.e. jank or latency. | + +### android.cujs.threads + +#### Views/Tables + +**android_jank_cuj_render_thread** + +TABLE +Table captures thread information for 'RenderThread' for all CUJs. + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| upid | JOINID(process.id) | process id. | +| utid | JOINID(thread.id) | thread id of the main/UI thread. | +| name | STRING | thread name. | +| track_id | JOINID(track.id) | track_id for the thread. | + +#### Table Functions + +**android_jank_cuj_app_thread** + +Returns a table with all CUJs and an additional column for the track id of thread_name +passed as parameter, if present in the same process of the cuj. + +| Argument | Type | Description | +|---|---|---| +| thread_name | STRING | Name of the thread for which information needs to be extracted. | + +| Column | Type | Description | +|---|---|---| +| cuj_id | LONG | Unique incremental ID for each CUJ. | +| upid | JOINID(process.id) | process id. | +| utid | LONG | thread id of the input thread. | +| name | STRING | name of the thread. | +| track_id | LONG | track id associated with the thread. | + +### android.desktop_mode + +#### Views/Tables + +**android_desktop_mode_windows** + +TABLE +Desktop Windows with durations they were open. + +| Column | Type | Description | +|---|---|---| +| raw_add_ts | TIMESTAMP | Window add timestamp; NULL if no add event in the trace. | +| raw_remove_ts | TIMESTAMP | Window remove timestamp; NULL if no remove event in the trace. | +| ts | TIMESTAMP | Timestamp that the window was added; or trace_start() if no add event in the trace. | +| dur | DURATION | Furation the window was open; or until trace_end() if no remove event in the trace. | +| instance_id | LONG | Desktop Window instance ID - unique per window. | +| uid | LONG | UID of the app running in the window. | + +### android.device + +#### Views/Tables + +**android_device_name** + +TABLE +Extract name of the device based on metadata from the trace. + +| Column | Type | Description | +|---|---|---| +| name | STRING | Device name. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | + +### android.dumpsys.show_map + +#### Views/Tables + +**android_dumpsys_show_map** + +TABLE +This table represents memory mapping information from /proc/\[pid\]/smaps +All memory values are in kilobytes (KB) + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process. | +| pid | JOINID(process.pid) | Process ID. | +| vss_kb | LONG | Virtual Set Size in kilobytes - total virtual memory mapped by the process. | +| rss_kb | LONG | Resident Set Size in kilobytes - actual physical memory used by the process. | +| pss_kb | LONG | Proportional Set Size in kilobytes - amount of memory shared with other processes. | +| shared_clean_kb | LONG | Clean shared pages in kilobytes - shared pages that haven't been modified. | +| shared_dirty_kb | LONG | Dirty shared pages in kilobytes - shared pages that have been modified. | +| private_clean_kb | LONG | Clean private pages in kilobytes - private pages that haven't been modified. | +| private_dirty_kb | LONG | Dirty private pages in kilobytes - private pages that have been modified. | +| swap_kb | LONG | Swap memory in kilobytes - memory that has been moved to swap space. | +| swap_pss_kb | LONG | Proportional Swap Size in kilobytes - swap shared with other processes. | +| anon_huge_pages_kb | LONG | Anonymous huge pages in kilobytes - large anonymous memory regions. | +| shmem_pmd_mapped_kb | LONG | Shared Memory PMD mapped in kilobytes - page middle directory mapped shared memory. | +| file_pmd_mapped_kb | LONG | File PMD mapped in kilobytes - page middle directory mapped file memory. | +| shared_huge_tlb_kb | LONG | Shared huge TLB in kilobytes - shared huge page table entries. | +| private_hugetlb_kb | LONG | Private huge TLB in kilobytes - private huge page table entries. | +| locked_kb | LONG | Locked memory in kilobytes - memory that can't be swapped out. | +| mapping_count | LONG | Number of mappings of the object. | +| mapped_object | STRING | Path to the mapped object (file, library, etc.). | + +### android.dvfs + +#### Views/Tables + +**android_dvfs_counters** + +VIEW +Dvfs counter with duration. + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| ts | TIMESTAMP | Timestamp when counter value changed. | +| value | DOUBLE | Counter value. | +| dur | DURATION | Counter duration. | + +**android_dvfs_counter_stats** + +TABLE +Aggregates dvfs counter slice for statistic. + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name on which all the other values are aggregated on. | +| max | DOUBLE | Max of all counter values for the counter name. | +| min | DOUBLE | Min of all counter values for the counter name. | +| dur | DURATION | Duration between the first and last counter value for the counter name. | +| wgt_avg | DOUBLE | Weighted avergate of all the counter values for the counter name. | + +**android_dvfs_counter_residency** + +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| value | DOUBLE | Counter value. | +| dur | DURATION | Counter duration. | +| pct | DOUBLE | Counter duration as a percentage of total duration. | + +### android.entity_state_residency + +#### Views/Tables + +**android_entity_state_residency** + +TABLE +Android entity state residency samples. +For details see: + +| Column | Type | Description | +|---|---|---| +| id | ID(counter.id) | `counter.id` | +| ts | TIMESTAMP | Timestamp of the residency sample. | +| dur | DURATION | Time until the next residency sample. | +| entity_name | STRING | Entity or subsytem name. | +| state_name | STRING | State name | +| raw_name | STRING | Raw name (alias of counter.name) | +| state_time_since_boot | DURATION | Time the entity or subsystem spent in the state since boot | +| state_time_since_boot_at_end | DURATION | Time the entity or subsystem spent in the state since boot on the next sample | +| state_time_ratio | DOUBLE | ratio of the time the entity or subsystem spend in the state out of the elapsed time of the sample period. A value of 1 typically means the 100% of time was spent in the state, and a value of 0 means no time was spent. | +| track_id | JOINID(track.id) | entity + state track id. Alias of `counter_track.id`. | + +### android.frames.jank_type + +#### Functions + +**android_is_sf_jank_type** + +Returns BOOL: True when the jank type represents sf jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +**android_is_app_jank_type** + +Returns BOOL: True when the jank type represents app jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +**android_is_missed_frame_type** + +Returns BOOL: True if jank_type represents missed frame jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +### android.frames.per_frame_metrics + +#### Views/Tables + +**android_frames_overrun** + +TABLE +The amount by which each frame missed of hit its deadline. Negative if the +deadline was not missed. Frames are considered janky if `overrun` is +positive. +Calculated as the difference between the end of the +`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the +frame. +Availability: from S (API 31). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| overrun | LONG | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | + +**android_frames_ui_time** + +TABLE +How much time did the frame's Choreographer callbacks take. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id | +| ui_time | LONG | UI time duration | + +**android_app_vsync_delay_per_frame** + +TABLE +App Vsync delay for a frame. The time between the VSYNC-app signal and the +start of Choreographer work. +Calculated as time difference between the actual frame start (from +`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` +slice. +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id | +| app_vsync_delay | LONG | App VSYNC delay. | + +**android_cpu_time_per_frame** + +TABLE +How much time did the frame take across the UI Thread + RenderThread. +Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice +duration and summed durations of all `DrawFrame` slices associated with this +frame. +Availability: from N (API 24). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id | +| app_vsync_delay | LONG | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | +| do_frame_dur | DURATION | Duration of `Choreographer#doFrame` slice. | +| draw_frame_dur | DURATION | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | +| cpu_time | LONG | CPU time across the UI Thread + RenderThread. | + +**android_frame_stats** + +TABLE +Aggregated stats of the frame. + +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| overrun | LONG | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | +| cpu_time | LONG | How much time did the frame take across the UI Thread + RenderThread. | +| ui_time | LONG | How much time did the frame's Choreographer callbacks take. | +| was_jank | BOOL | Was frame janky. | +| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | +| was_big_jank | BOOL | CPU time of the frame took over 50ms. | +| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | + +### android.frames.timeline + +#### Views/Tables + +**android_frames_choreographer_do_frame** + +TABLE +All of the `Choreographer#doFrame` slices with their frame id. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Choreographer#doFrame slice. Slice with the name "Choreographer#doFrame {frame id}". | +| frame_id | LONG | Frame id. Taken as the value behind "Choreographer#doFrame" in slice name. | +| ui_thread_utid | JOINID(thread.id) | Utid of the UI thread | +| upid | JOINID(process.id) | Upid of application process | +| ts | TIMESTAMP | Timestamp of the slice. | + +**android_frames_draw_frame** + +TABLE +All of the `DrawFrame` slices with their frame id and render thread. +There might be multiple DrawFrames slices for a single vsync (frame id). +This happens when we are drawing multiple layers (e.g. status bar and +notifications). + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | DrawFrame slice. Slice with the name "DrawFrame {frame id}". | +| frame_id | LONG | Frame id. Taken as the value behind "DrawFrame" in slice name. | +| render_thread_utid | JOINID(thread.id) | Utid of the render thread | +| upid | JOINID(process.id) | Upid of application process | + +**android_frames_layers** + +TABLE +TODO(b/384322064) Match actual timeline slice with correct draw frame using layer name. +All slices related to one frame. Aggregates `Choreographer#doFrame`, +`actual_frame_timeline_slice` and `expected_frame_timeline_slice` slices. +This table differs slightly from the android_frames table, as it +captures the layer_id for each actual timeline slice too. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| ts | TIMESTAMP | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | +| dur | DURATION | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | +| ts_end | TIMESTAMP | End timestamp of the frame. End of the frame as defined by the sum of start timestamp and duration of the frame. | +| do_frame_id | JOINID(slice.id) | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | JOINID(slice.id) | `slice.id` of "DrawFrame" slice. For now, we only support the first DrawFrame slice (due to b/384322064). | +| actual_frame_timeline_id | JOINID(slice.id) | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | JOINID(slice.id) | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | +| ui_thread_utid | JOINID(thread.id) | thread id of the UI thread. | +| layer_id | LONG | layer id associated with the actual frame. | +| layer_name | STRING | layer name associated with the actual frame. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | + +**android_frames** + +TABLE +Table based on the android_frames_layers table. It aggregates time, duration and counts +information across different layers for a given frame_id in a given process. + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| ts | TIMESTAMP | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | +| dur | DURATION | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | +| do_frame_id | JOINID(slice.id) | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | JOINID(slice.id) | `slice.id` of "DrawFrame" slice. For now, we only support the first DrawFrame slice (due to b/384322064). | +| actual_frame_timeline_id | JOINID(slice.id) | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | JOINID(slice.id) | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | +| ui_thread_utid | JOINID(thread.id) | thread id of the UI thread. | +| actual_frame_timeline_count | LONG | Count of slices in `actual_frame_timeline_slice` related to this frame. | +| expected_frame_timeline_count | LONG | Count of slices in `expected_frame_timeline_slice` related to this frame. | +| draw_frame_count | LONG | Count of draw_frame associated to this frame. | +| upid | JOINID(process.id) | process id. | +| process_name | STRING | process name. | + +#### Table Functions + +**android_first_frame_after** + +Returns first frame after the provided timestamp. The returning table has at +most one row. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | + +| Column | Type | Description | +|---|---|---| +| frame_id | LONG | Frame id. | +| ts | TIMESTAMP | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | +| dur | DURATION | Duration of the frame. | +| do_frame_id | JOINID(slice.id) | "Choreographer#doFrame" slice. The slice with name "Choreographer#doFrame" corresponding to this frame. | +| draw_frame_id | JOINID(slice.id) | "DrawFrame" slice. The slice with name "DrawFrame" corresponding to this frame. | +| actual_frame_timeline_id | JOINID(slice.id) | actual_frame_timeline_slice\` slice related to this frame. | +| expected_frame_timeline_id | JOINID(slice.id) | `expected_frame_timeline_slice` slice related to this frame. | +| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | +| ui_thread_utid | JOINID(thread.id) | `utid` of the UI thread. | + +### android.freezer + +#### Views/Tables + +**android_freezer_events** + +TABLE +All frozen processes and their frozen duration. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of frozen process | +| pid | LONG | Pid of frozen process | +| ts | TIMESTAMP | Timestamp process was frozen. | +| dur | DURATION | Duration process was frozen for. | +| unfreeze_reason_int | LONG | Unfreeze reason Integer. | +| unfreeze_reason_str | STRING | Unfreeze reason String. | + +### android.garbage_collection + +#### Views/Tables + +**android_garbage_collection_events** + +TABLE +All Garbage collection events with a breakdown of the time spent and heap reclaimed. + +| Column | Type | Description | +|---|---|---| +| tid | LONG | Tid of thread running garbage collection. | +| pid | LONG | Pid of process running garbage collection. | +| utid | JOINID(thread.id) | Utid of thread running garbage collection. | +| upid | JOINID(process.id) | Upid of process running garbage collection. | +| thread_name | STRING | Name of thread running garbage collection. | +| process_name | STRING | Name of process running garbage collection. | +| gc_type | STRING | Type of garbage collection. | +| is_mark_compact | LONG | Whether gargage collection is mark compact or copying. | +| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | +| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | +| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | +| gc_id | LONG | Garbage collection id. | +| gc_ts | TIMESTAMP | Garbage collection timestamp. | +| gc_dur | DURATION | Garbage collection wall duration. | +| gc_running_dur | DURATION | Garbage collection duration spent executing on CPU. | +| gc_runnable_dur | DURATION | Garbage collection duration spent waiting for CPU. | +| gc_unint_io_dur | DURATION | Garbage collection duration spent waiting in the Linux kernel on IO. | +| gc_unint_non_io_dur | DURATION | Garbage collection duration spent waiting in the Linux kernel without IO. | +| gc_int_dur | LONG | Garbage collection duration spent waiting in interruptible sleep. | + +### android.gpu.frequency + +#### Views/Tables + +**android_gpu_frequency** + +TABLE +GPU frequency counter per GPU. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| gpu_id | LONG | GPU id. Joinable with `gpu_counter_track.gpu_id`. | +| gpu_freq | LONG | GPU frequency | +| prev_gpu_freq | LONG | GPU frequency from previous slice | +| next_gpu_freq | LONG | GPU frequency from next slice | + +### android.gpu.mali_power_state + +#### Views/Tables + +**android_mali_gpu_power_state** + +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| power_state | LONG | GPU power state | + +### android.gpu.memory + +#### Views/Tables + +**android_gpu_memory_per_process** + +TABLE +Counter for GPU memory per process with duration. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| upid | JOINID(process.id) | Upid of the process | +| gpu_memory | LONG | GPU memory | + +### android.gpu.work_period + +#### Views/Tables + +**android_gpu_work_period_track** + +TABLE +Tracks for GPU work period events originating from the +`power/gpu_work_period` Linux ftrace tracepoint. + +This tracepoint is usually only available on selected Android devices. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique identifier for this track. Joinable with track.id. | +| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | +| uid | LONG | The UID of the package for which the GPU work period events were emitted. | +| gpu_id | LONG | The GPU identifier for which the GPU work period events were emitted. | + +### android.input + +#### Views/Tables + +**android_input_events** + +TABLE +All input events with round trip latency breakdown. Input delivery is socket based and every +input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure +latencies between: + +1. Input dispatch event sent from OS. +2. Input dispatch event received in app. +3. Input ACK event sent from app. +4. Input ACK event received in OS. + +| Column | Type | Description | +|---|---|---| +| dispatch_latency_dur | DURATION | Duration from input dispatch to input received. | +| handling_latency_dur | DURATION | Duration from input received to input ACK sent. | +| ack_latency_dur | DURATION | Duration from input ACK sent to input ACK received. | +| total_latency_dur | DURATION | Duration from input dispatch to input event ACK received. | +| end_to_end_latency_dur | DURATION | Duration from input read to frame present time. Null if an input event has no associated frame event. | +| tid | LONG | Tid of thread receiving the input event. | +| thread_name | STRING | Name of thread receiving the input event. | +| pid | LONG | Pid of process receiving the input event. | +| process_name | STRING | Name of process receiving the input event. | +| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | +| event_action | STRING | Input event action. | +| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | +| event_channel | STRING | Input event channel name. | +| input_event_id | STRING | Unique identifier for the input event. | +| read_time | LONG | Timestamp input event was read by InputReader. | +| dispatch_track_id | JOINID(track.id) | Thread track id of input event dispatching thread. | +| dispatch_ts | TIMESTAMP | Timestamp input event was dispatched. | +| dispatch_dur | DURATION | Duration of input event dispatch. | +| receive_track_id | JOINID(track.id) | Thread track id of input event receiving thread. | +| receive_ts | TIMESTAMP | Timestamp input event was received. | +| receive_dur | DURATION | Duration of input event receipt. | +| frame_id | LONG | Vsync Id associated with the input. Null if an input event has no associated frame event. | + +**android_key_events** + +VIEW +Key events processed by the Android framework (from android.input.inputevent data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the trace entry | +| event_id | LONG | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | TIMESTAMP | The timestamp of when the input event was processed by the system | +| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | +| source | LONG | Event source e.g. touchscreen, keyboard | +| action | LONG | Action e.g. down, move | +| device_id | LONG | Device id | +| display_id | LONG | Display id | +| key_code | LONG | Key code | + +**android_motion_events** + +VIEW +Motion events processed by the Android framework (from android.input.inputevent data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the trace entry | +| event_id | LONG | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | TIMESTAMP | The timestamp of when the input event was processed by the system | +| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | +| source | LONG | Event source e.g. touchscreen, keyboard | +| action | LONG | Action e.g. down, move | +| device_id | LONG | Device id | +| display_id | LONG | Display id | + +**android_input_event_dispatch** + +VIEW +Input event dispatching information in Android (from android.input.inputevent data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | ID of the trace entry | +| event_id | LONG | Event ID of the input event that was dispatched | +| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | +| vsync_id | LONG | Vsync ID that identifies the state of the windows during which the dispatch decision was made | +| window_id | LONG | Window ID of the window receiving the event | + +### android.job_scheduler + +#### Views/Tables + +**android_job_scheduler_events** + +TABLE +All scheduled jobs and their latencies. + +The table is populated by ATrace using the system server ATrace category +(`atrace_categories: "ss"`). You can also set the `atrace_apps` of interest. + +This differs from the `android_job_scheduler_states` table +in the `android.job_scheduler_states` module which is populated +by the `ScheduledJobStateChanged` atom. + +Using `android_job_scheduler_states` is preferred when the +`ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in the trace since +it includes the constraint, screen, or charging state changes for +each job in a trace. + +| Column | Type | Description | +|---|---|---| +| job_id | LONG | Id of the scheduled job assigned by the app developer. | +| uid | LONG | Uid of the process running the scheduled job. | +| package_name | STRING | Package name of the process running the scheduled job. | +| job_service_name | STRING | Service component name of the scheduled job. | +| track_id | JOINID(track.id) | Thread track id of the job scheduler event slice. | +| id | LONG | Slice id of the job scheduler event slice. | +| ts | TIMESTAMP | Timestamp the job was scheduled. | +| dur | DURATION | Duration of the scheduled job. | + +### android.job_scheduler_states + +#### Views/Tables + +**android_job_scheduler_states** + +TABLE +This table returns constraint changes that a +job will go through in a single trace. + +Values in this table are derived from the the `ScheduledJobStateChanged` +atom. This table differs from the +`android_job_scheduler_with_screen_charging_states` in this module +(`android.job_scheduler_states`) by only having job constraint information. + +See documentation for the `android_job_scheduler_with_screen_charging_states` +for how tables in this module differ from `android_job_scheduler_events` +table in the `android.job_scheduler` module and how to populate this table. + +| Column | Type | Description | +|---|---|---| +| id | ID | Unique identifier for job scheduler state. | +| ts | TIMESTAMP | Timestamp of job state slice. | +| dur | DURATION | Duration of job state slice. | +| slice_id | JOINID(slice.id) | Id of the slice. | +| job_name | STRING | Name of the job (as named by the app). | +| uid | LONG | Uid associated with job. | +| job_id | LONG | Id of job (assigned by app for T- builds and system generated in U+ builds). | +| package_name | STRING | Package that the job belongs (ex: associated app). | +| job_namespace | STRING | Namespace of job. | +| effective_priority | LONG | Priority at which JobScheduler ran the job. | +| has_battery_not_low_constraint | BOOL | True if app requested job should run when the device battery is not low. | +| has_charging_constraint | BOOL | True if app requested job should run when the device is charging. | +| has_connectivity_constraint | BOOL | True if app requested job should run when device has connectivity. | +| has_content_trigger_constraint | BOOL | True if app requested job should run when there is a content trigger. | +| has_deadline_constraint | BOOL | True if app requested there is a deadline by which the job should run. | +| has_idle_constraint | BOOL | True if app requested job should run when device is idle. | +| has_storage_not_low_constraint | BOOL | True if app requested job should run when device storage is not low. | +| has_timing_delay_constraint | BOOL | True if app requested job has a timing delay. | +| is_prefetch | BOOL | True if app requested job should run within hours of app launch. | +| is_requested_expedited_job | BOOL | True if app requested that the job is run as an expedited job. | +| is_running_as_expedited_job | BOOL | The job is run as an expedited job. | +| num_previous_attempts | TIMESTAMP | Number of previous attempts at running job. | +| requested_priority | LONG | The requested priority at which the job should run. | +| standby_bucket | STRING | The job's standby bucket (one of: Active, Working Set, Frequent, Rare, Never, Restricted, Exempt). | +| is_periodic | BOOL | Job should run in intervals. | +| has_flex_constraint | BOOL | True if the job should run as a flex job. | +| is_requested_as_user_initiated_job | BOOL | True is app has requested that a job be run as a user initiated job. | +| is_running_as_user_initiated_job | BOOL | True if job is running as a user initiated job. | +| deadline_ms | LONG | Deadline that job has requested and valid if has_deadline_constraint is true. | +| job_start_latency_ms | LONG | The latency in ms between when a job is scheduled and when it actually starts. | +| num_uncompleted_work_items | LONG | Number of uncompleted job work items. | +| proc_state | STRING | Process state of the process responsible for running the job. | +| internal_stop_reason | STRING | Internal stop reason for a job. | +| public_stop_reason | STRING | Public stop reason for a job. | + +**android_job_scheduler_with_screen_charging_states** + +TABLE +This table returns the constraint, charging, +and screen state changes that a job will go through +in a single trace. + +Values from this table are derived from +the `ScheduledJobStateChanged` atom. This differs from the +`android_job_scheduler_events` table in the `android.job_scheduler` module +which is derived from ATrace the system server category +(`atrace_categories: "ss"`). + +This also differs from the `android_job_scheduler_states` in this module +(`android.job_scheduler_states`) by providing charging and screen state +changes. + +To populate this table, enable the Statsd Tracing Config with the +ATOM_SCHEDULED_JOB_STATE_CHANGED push atom id. + + +This table is preferred over `android_job_scheduler_events` +since it contains more information and should be used whenever +`ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in a trace. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of job. | +| dur | DURATION | Duration of slice in ns. | +| slice_id | JOINID(slice.id) | Id of the slice. | +| job_name | STRING | Name of the job (as named by the app). | +| job_id | LONG | Id of job (assigned by app for T- builds and system generated in U+ builds). | +| uid | LONG | Uid associated with job. | +| job_dur | DURATION | Duration of entire job in ns. | +| package_name | STRING | Package that the job belongs (ex: associated app). | +| job_namespace | STRING | Namespace of job. | +| charging_state | STRING | Device charging state during job (one of: Charging, Discharging, Not charging, Full, Unknown). | +| screen_state | STRING | Device screen state during job (one of: Screen off, Screen on, Always-on display (doze), Unknown). | +| effective_priority | LONG | Priority at which JobScheduler ran the job. | +| has_battery_not_low_constraint | BOOL | True if app requested job should run when the device battery is not low. | +| has_charging_constraint | BOOL | True if app requested job should run when the device is charging. | +| has_connectivity_constraint | BOOL | True if app requested job should run when device has connectivity. | +| has_content_trigger_constraint | BOOL | True if app requested job should run when there is a content trigger. | +| has_deadline_constraint | BOOL | True if app requested there is a deadline by which the job should run. | +| has_idle_constraint | BOOL | True if app requested job should run when device is idle. | +| has_storage_not_low_constraint | BOOL | True if app requested job should run when device storage is not low. | +| has_timing_delay_constraint | BOOL | True if app requested job has a timing delay. | +| is_prefetch | BOOL | True if app requested job should run within hours of app launch. | +| is_requested_expedited_job | BOOL | True if app requested that the job is run as an expedited job. | +| is_running_as_expedited_job | BOOL | The job is run as an expedited job. | +| num_previous_attempts | TIMESTAMP | Number of previous attempts at running job. | +| requested_priority | LONG | The requested priority at which the job should run. | +| standby_bucket | STRING | The job's standby bucket (one of: Active, Working Set, Frequent, Rare, Never, Restricted, Exempt). | +| is_periodic | BOOL | Job should run in intervals. | +| has_flex_constraint | BOOL | True if the job should run as a flex job. | +| is_requested_as_user_initiated_job | BOOL | True is app has requested that a job be run as a user initiated job. | +| is_running_as_user_initiated_job | BOOL | True if job is running as a user initiated job. | +| deadline_ms | LONG | Deadline that job has requested and valid if has_deadline_constraint is true. | +| job_start_latency_ms | LONG | The latency in ms between when a job is scheduled and when it actually starts. | +| num_uncompleted_work_items | LONG | Number of uncompleted job work items. | +| proc_state | STRING | Process state of the process responsible for running the job. | +| internal_stop_reason | STRING | Internal stop reason for a job. | +| public_stop_reason | STRING | Public stop reason for a job. | + +### android.kernel_wakelocks + +#### Views/Tables + +**android_kernel_wakelocks** + +TABLE +Table of kernel (or native) wakelocks with held duration. + +Subtracts suspended time from each period to calculate the +fraction of awake time for which the wakelock was held. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration. | +| awake_dur | DURATION | Duration spent awake. | +| name | STRING | Kernel or native wakelock name. | +| type | STRING | 'kernel' or 'native'. | +| held_dur | DURATION | Time the wakelock was held. | +| held_ratio | DOUBLE | Fraction of awake (not suspended) time the wakelock was held. | + +### android.memory.dmabuf + +#### Views/Tables + +**android_dmabuf_allocs** + +TABLE +Track dmabuf allocations, re-attributing gralloc allocations to their source +(if binder transactions to gralloc are recorded). + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | timestamp of the allocation | +| buf_size | LONG | allocation size (will be negative for release) | +| inode | LONG | dmabuf inode | +| utid | JOINID(thread.id) | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | +| tid | LONG | tid of thread responsible for the allocation | +| thread_name | STRING | thread name | +| upid | JOINID(process.id) | upid of process responsible for the allocation (matches utid) | +| pid | LONG | pid of process responsible for the allocation | +| process_name | STRING | process name | + +**android_memory_cumulative_dmabuf** + +TABLE +Provides a timeseries of dmabuf allocations for each process. +To populate this table, tracing must be enabled with the "dmabuf_allocs" ftrace event. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | upid of process responsible for the allocation (matches utid) | +| process_name | STRING | process name | +| utid | JOINID(thread.id) | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | +| thread_name | STRING | thread name | +| ts | TIMESTAMP | timestamp of the allocation | +| value | LONG | total allocation size per process and thread | + +### android.memory.heap_graph.class_relationship + +#### Macros + +**android_heap_graph_class_find_descendants** + +Given a list of classes as ancestor classes, return all the classes that +descend from them. +Returns: TableOrSubquery, Table of the schema (id JOINID(heap_graph_class.id), ancestor_class_id JOINID(heap_graph_class.id), ancestor_class_name STRING) id: `id` of the class as in heap_graph_class ancestor_class_id: `id` of the ancestor class as given in the input ancestor_class_name: `name` of the ancestor class as in heap_graph_class + +| Argument | Type | Description | +|---|---|---| +| ancestor_class_ids | TableOrSubquery | ancestor class `id`s from the heap_graph_class table containing a single column: `id` | + +### android.memory.heap_graph.class_summary_tree + +#### Views/Tables + +**android_heap_graph_class_summary_tree** + +TABLE +Table containing all the Android heap graphs in the trace converted to a +shortest-path tree and then aggregated by class name. + +This table contains a "flamegraph-like" representation of the contents of the +heap graph. + +| Column | Type | Description | +|---|---|---| +| graph_sample_ts | TIMESTAMP | The timestamp the heap graph was dumped at. | +| upid | JOINID(process.id) | The upid of the process. | +| id | LONG | The id of the node in the class tree. | +| parent_id | LONG | The parent id of the node in the class tree or NULL if this is the root. | +| name | STRING | The name of the class. | +| root_type | STRING | A string describing the type of Java root if this node is a root or NULL if this node is not a root. | +| self_count | LONG | The count of objects with the same class name and the same path to the root. | +| self_size | LONG | The size of objects with the same class name and the same path to the root. | +| cumulative_count | LONG | The sum of `self_count` of this node and all descendants of this node. | +| cumulative_size | LONG | The sum of `self_size` of this node and all descendants of this node. | + +### android.memory.heap_graph.dominator_tree + +#### Views/Tables + +**heap_graph_dominator_tree** + +TABLE +All reachable heap graph objects, their immediate dominators and summary of +their dominated sets. +The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. +Each reachable object is a node in the dominator tree, their immediate +dominator is their parent node in the tree, and their dominated set is all +their descendants in the tree. All size information come from the +heap_graph_object prelude table. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Heap graph object id. | +| idom_id | LONG | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | +| dominated_obj_count | LONG | Count of all objects dominated by this object, self inclusive. | +| dominated_size_bytes | LONG | Total self_size of all objects dominated by this object, self inclusive. | +| dominated_native_size_bytes | LONG | Total native_size of all objects dominated by this object, self inclusive. | +| depth | LONG | Depth of the object in the dominator tree. Depth of root objects are 1. | + +### android.memory.heap_graph.heap_graph_class_aggregation + +#### Views/Tables + +**android_heap_graph_class_aggregation** + +TABLE +Class-level breakdown of the java heap. +Per type name aggregates the object stats and the dominator tree stats. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Process upid | +| graph_sample_ts | TIMESTAMP | Heap dump timestamp | +| type_id | LONG | Class type id | +| type_name | STRING | Class name (deobfuscated if available) | +| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | +| obj_count | LONG | Count of class instances | +| size_bytes | LONG | Size of class instances | +| native_size_bytes | LONG | Native size of class instances | +| reachable_obj_count | LONG | Count of reachable class instances | +| reachable_size_bytes | LONG | Size of reachable class instances | +| reachable_native_size_bytes | LONG | Native size of reachable class instances | +| dominated_obj_count | LONG | Count of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_size_bytes | LONG | Size of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_native_size_bytes | LONG | Native size of all objects dominated by instances of this class Only applies to reachable objects | + +### android.memory.heap_graph.heap_graph_stats + +#### Views/Tables + +**android_heap_graph_stats** + +TABLE +Table summarizing java heap graphs collected with the ART perfetto module. +Contains one row per heap graph, with summary statistics (e.g. total / reachable objects) +and memory stats for the corresponding process at the time of the heap dump. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | The upid of the process. | +| graph_sample_ts | TIMESTAMP | The timestamp the heap graph was dumped at. | +| process_uptime | LONG | The uptime of the process at the time of the heap graph dump. | +| total_heap_size | LONG | The total (reachable + unreachable) size of the Java heap in bytes. | +| total_native_alloc_registry_size | LONG | The total size of native allocations (registered with NativeAllocationRegistry) in bytes. Does *not* overlap with total_heap_size. | +| total_obj_count | LONG | The number of objects in the heap. | +| reachable_heap_size | LONG | The reachable size of the Java heap in bytes. | +| reachable_native_alloc_registry_size | LONG | The size of reachable native allocations (registered with NativeAllocationRegistry) in bytes. | +| reachable_obj_count | LONG | The number of reachable objects in the heap. | +| oom_score_adj | LONG | The OOM score adj of the process at the time of the heap graph dump. | +| anon_rss_and_swap_size | LONG | The anon RSS + swap size of the process (in bytes) at the time of the heap graph dump. | +| dmabuf_rss_size | LONG | The dmabuf size of the process (in bytes) at the time of the heap graph dump. | + +### android.memory.heap_profile.summary_tree + +#### Views/Tables + +**android_heap_profile_summary_tree** + +TABLE +Table summarising the amount of memory allocated by each +callstack as seen by Android native heap profiling (i.e. +profiling information collected by heapprofd). + +> [!NOTE] +> **Note:** this table collapses data from all processes together into a single table. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | LONG | The id of the parent callstack for this callstack. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | LONG | The line number in the file the function is located at. | +| self_size | LONG | The amount of memory allocated and *not freed* with this function as the leaf frame. | +| cumulative_size | LONG | The amount of memory allocated and *not freed* with this function appearing anywhere on the callstack. | +| self_alloc_size | LONG | The amount of memory allocated with this function as the leaf frame. This may include memory which was later freed. | +| cumulative_alloc_size | LONG | The amount of memory allocated with this function appearing anywhere on the callstack. This may include memory which was later freed. | + +### android.memory.lmk + +#### Views/Tables + +**android_lmk_events** + +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | timestamp of the kill being requested by lmkd | +| upid | JOINID(process.id) | upid of the process being killed | +| pid | LONG | pid of the process being killed | +| process_name | STRING | process name of the process being killed | +| oom_score_adj | LONG | oom_score_adj of the process being killed | +| kill_reason | STRING | lmkd kill_reason (matches lmkd/statslog.h kill_reasons enum) | +| kill_reason_raw | LONG | lmkd kill_reason enum value | + +### android.memory.process + +#### Views/Tables + +**memory_oom_score_with_rss_and_swap_per_process** + +TABLE +Process memory and it's OOM adjuster scores. Detects transitions, each new +interval means that either the memory or OOM adjuster score of the process changed. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp the oom_adj score or memory of the process changed | +| dur | DURATION | Duration until the next oom_adj score or memory change of the process. | +| score | LONG | oom adjuster score of the process. | +| bucket | STRING | oom adjuster bucket of the process. | +| upid | JOINID(process.id) | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| pid | LONG | Pid of the process having an oom_adj update. | +| oom_adj_id | JOINID(slice.id) | Slice of the latest oom_adj update in the system_server. | +| oom_adj_ts | TIMESTAMP | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | DURATION | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | JOINID(track.id) | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | +| anon_rss | LONG | Anon RSS counter value | +| file_rss | LONG | File RSS counter value | +| shmem_rss | LONG | Shared memory RSS counter value | +| rss | LONG | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | LONG | Swap counter value | +| anon_rss_and_swap | LONG | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | LONG | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +### android.monitor_contention + +#### Views/Tables + +**android_monitor_contention** + +TABLE +Contains parsed monitor contention slices. + +| Column | Type | Description | +|---|---|---| +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | LONG | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | JOINID(thread.id) | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | JOINID(thread.id) | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | LONG | Tid of thread holding the lock. | +| upid | JOINID(process.id) | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | LONG | Slice id of lock contention. | +| ts | TIMESTAMP | Timestamp of lock contention start. | +| dur | DURATION | Wall clock duration of lock contention. | +| monotonic_dur | DURATION | Monotonic clock duration of lock contention. | +| track_id | JOINID(track.id) | Thread track id of blocked thread. | +| is_blocked_thread_main | LONG | Whether the blocked thread is the main thread. | +| blocked_thread_tid | LONG | Tid of the blocked thread | +| is_blocking_thread_main | LONG | Whether the blocking thread is the main thread. | +| blocking_thread_tid | LONG | Tid of thread holding the lock. | +| binder_reply_id | LONG | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | TIMESTAMP | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | LONG | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | LONG | Pid of process experiencing lock contention. | + +**android_monitor_contention_chain** + +TABLE +Contains parsed monitor contention slices with the parent-child relationships. + +| Column | Type | Description | +|---|---|---| +| parent_id | LONG | Id of monitor contention slice blocking this contention. | +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | LONG | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | JOINID(thread.id) | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | JOINID(thread.id) | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | LONG | Tid of thread holding the lock. | +| upid | JOINID(process.id) | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | LONG | Slice id of lock contention. | +| ts | TIMESTAMP | Timestamp of lock contention start. | +| dur | DURATION | Wall clock duration of lock contention. | +| monotonic_dur | DURATION | Monotonic clock duration of lock contention. | +| track_id | JOINID(track.id) | Thread track id of blocked thread. | +| is_blocked_thread_main | LONG | Whether the blocked thread is the main thread. | +| blocked_thread_tid | LONG | Tid of the blocked thread | +| is_blocking_thread_main | LONG | Whether the blocking thread is the main thread. | +| blocking_thread_tid | LONG | Tid of thread holding the lock. | +| binder_reply_id | LONG | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | TIMESTAMP | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | LONG | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | LONG | Pid of process experiencing lock contention. | +| child_id | LONG | Id of monitor contention slice blocked by this contention. | + +**android_monitor_contention_chain_thread_state** + +TABLE +Contains the span join of the first waiters in the \|android_monitor_contention_chain\| with their +blocking_thread thread state. + +Note that we only span join the duration where the lock was actually held and contended. +This can be less than the duration the lock was 'waited on' when a different waiter acquired the +lock earlier than the first waiter. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice id of lock contention. | +| ts | TIMESTAMP | Timestamp of lock contention start. | +| dur | DURATION | Wall clock duration of lock contention. | +| blocking_utid | JOINID(thread.id) | Utid of the blocking | +| blocked_function | STRING | Blocked kernel function of the blocking thread. | +| state | STRING | Thread state of the blocking thread. | + +**android_monitor_contention_chain_thread_state_by_txn** + +VIEW +Aggregated thread_states on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention slice, it returns the aggregated sum of all the thread states on the +blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice id of the monitor contention. | +| thread_state | STRING | A | +| thread_state_dur | DURATION | Total time the blocking thread spent in the | +| thread_state_count | LONG | Count of all times the blocking thread entered | + +**android_monitor_contention_chain_blocked_functions_by_txn** + +VIEW +Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention, it returns the aggregated sum of all the kernel +blocked function durations on the blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice id of the monitor contention. | +| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | +| blocked_function_dur | DURATION | Total time the blocking thread spent in the | +| blocked_function_count | LONG | Count of all times the blocking thread executed the | + +#### Functions + +**android_extract_android_monitor_contention_blocking_thread** + +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_blocking_tid** + +Returns LONG: Blocking thread tid + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_blocking_method** + +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_short_blocking_method** + +Extracts a shortened form of the blocking method name from a slice name. +The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_blocked_method** + +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_short_blocked_method** + +Extracts a shortened form of the monitor contention blocked method name +from a slice name. The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_waiter_count** + +Returns LONG: Count of waiters on the lock + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_blocking_src** + +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +**android_extract_android_monitor_contention_blocked_src** + +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +#### Table Functions + +**android_monitor_contention_graph** + +Returns a DAG of all Java lock contentions in a process. +Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. + +| Argument | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of process to generate a lock graph for. | + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of lock graph. | + +### android.network_packets + +#### Views/Tables + +**android_network_packets** + +VIEW +Android network packet events (from android.network_packets data source). + +| Column | Type | Description | +|---|---|---| +| id | ID | Id of the slice. | +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration (non-zero only in aggregate events) | +| track_name | STRING | The track name (interface and direction) | +| package_name | STRING | Traffic package source (or uid=$X if not found) | +| iface | STRING | Traffic interface name (linux interface name) | +| direction | STRING | Traffic direction ('Transmitted' or 'Received') | +| packet_count | LONG | Number of packets in this event | +| packet_length | LONG | Number of bytes in this event (wire size) | +| packet_transport | STRING | Transport used for traffic in this event | +| packet_tcp_flags | LONG | TCP flags used by tcp frames in this event | +| socket_tag | STRING | The Android traffic tag of the network socket | +| socket_uid | LONG | The Linux user id of the network socket | +| local_port | LONG | The local port number (for udp or tcp only) | +| remote_port | LONG | The remote port number (for udp or tcp only) | +| packet_icmp_type | LONG | 1-byte ICMP type identifier. | +| packet_icmp_code | LONG | 1-byte ICMP code identifier. | +| packet_tcp_flags_int | LONG | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | +| socket_tag_int | LONG | Packet's socket tag as an integer. | + +#### Macros + +**android_network_uptime_spans** + +Computes network uptime spans based on an idle timeout model. + +It is common in networking to have an interface active for some time after +use. For example, mobile networks are often connected for 10 or more seconds +after the last packet is sent or received. This macro simulates this timeout +and returns spans that approximate the underlying connected regions. +Returns: TableOrSubquery, + +| Argument | Type | Description | +|---|---|---| +| src | TableOrSubquery | A table/view/subquery containing the network events to apply the idle timeout model to. The table must contain all partition_columns, ts, dur, packet_count, and packet_length. | +| partition_columns | ColumnNameList | A parenthesized set of columns to partition the analysis by. | +| timeout | Expr | The idle timeout, expressed in nanoseconds. | + +**android_network_uptime_cost** + +Compute the per-row uptime cost of network activity. + +It is common in networking to have an interface active for some time after +use. For example, mobile networks are often connected for 10 or more seconds +after the last packet is sent or received. This macro computes a cost factor +indicating how much each row impacts the idle timer. + +For example, assuming a 10s timeout, the first packet will extend the timeout +10s in the future, and be assigned 10s of cost. If a packet arrives 4s later, +it pushes the timer an additional 4s, receiving 4s of cost. In this simple +case, cost is MIN(ts-last_packet_ts, timeout). + +The complication is that network events can be aggregates, with more than one +packet. In such cases, we end up with a span with non-zero duration, rather +than an instant, and no easy way to compute time since the last packet. + +The solution is to detect overlap regions and compute cost for the region as +a whole. The first event in each group receives the standard uptime cost as +described above. Each group has an additional cost equal to the duration of +the group which is distributed using packet count as weight. + +For example (times in seconds, no partition, and 10 second timeout): + +`ts=5, dur=0, packet_count=1 -> group=1, uptime_cost=10 +ts=7, dur=0, packet_count=1 -> group=2, uptime_cost=2 +ts=20, dur=5, packet_count=9 -> group=3, uptime_cost=14.5 +ts=22, dur=0, packet_count=1 -> group=3, uptime_cost=0.5` The third group spans ts=20 to ts=25, with a timeout at ts=35. This gives the +group a total cost of 15 which is distributed between the two rows. The 3rd +row receives 10s for being first, and 9/10 the duration cost (5\*9/10=4.5). + +The returned table schema is (id ID, uptime_cost INT64) where uptime cost is +in nanoseconds. +Returns: TableOrSubquery, + +| Argument | Type | Description | +|---|---|---| +| src | TableOrSubquery | A table/view/subquery containing the network events to apply the idle timeout model to. The table must contain all partition_columns, id, ts, dur, and packet_count. | +| partition_columns | ColumnNameList | A parenthesized set of columns to partition the analysis by. | +| timeout | Expr | The idle timeout, expressed in nanoseconds. | + +### android.oom_adjuster + +#### Views/Tables + +**android_oom_adj_intervals** + +VIEW +All oom adj state intervals across all processes along with the reason for the state update. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp the oom_adj score of the process changed | +| dur | DURATION | Duration until the next oom_adj score change of the process. | +| score | LONG | oom_adj score of the process. | +| bucket | STRING | oom_adj bucket of the process. | +| upid | JOINID(process.id) | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| oom_adj_id | LONG | Slice id of the latest oom_adj update in the system_server. | +| oom_adj_ts | TIMESTAMP | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | DURATION | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | JOINID(track.id) | Track id of the latest oom_adj update in the system_server | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | + +#### Functions + +**android_oom_adj_score_to_bucket_name** + +Converts an oom_adj score Integer to String sample name. +One of: cached, background, job, foreground_service, bfgs, foreground and +system. +Returns STRING: Returns the sample bucket based on the oom score. + +| Argument | Type | Description | +|---|---|---| +| oom_score | LONG | `oom_score` value | + +**android_oom_adj_score_to_detailed_bucket_name** + +Converts an oom_adj score Integer to String bucket name. +Deprecated: use `android_oom_adj_score_to_bucket_name` instead. +Returns STRING: Returns the oom_adj bucket. + +| Argument | Type | Description | +|---|---|---| +| value | LONG | oom_adj score. | +| android_appid | LONG | android_app id of the process. | + +### android.power_rails + +#### Views/Tables + +**android_power_rails_counters** + +TABLE +Android power rails counters data. +For details see: +NOTE: Requires dedicated hardware - table is only populated on Pixels. + +| Column | Type | Description | +|---|---|---| +| id | ID(counter.id) | `counter.id` | +| ts | TIMESTAMP | Timestamp of the energy measurement. | +| dur | DURATION | Time until the next energy measurement. | +| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | +| raw_power_rail_name | STRING | Raw power rail name. | +| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | +| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | +| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | +| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | +| track_id | JOINID(track.id) | Power rail track id. Alias of `counter_track.id`. | +| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | + +**android_power_rails_metadata** + +TABLE +High level metadata about each of the power rails. + +| Column | Type | Description | +|---|---|---| +| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | +| raw_power_rail_name | STRING | Raw power rail name from the hardware. | +| friendly_name | STRING | User-friendly name for the power rail. | +| track_id | JOINID(track.id) | Power rail track id. Alias of `counter_track.id`. | +| subsystem_name | STRING | Subsystem name that this power rail belongs to. | +| machine_id | JOINID(machine.id) | The device the power rail is associated with. | + +### android.process_metadata + +#### Views/Tables + +**android_process_metadata** + +TABLE +Data about packages running on the process. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Process upid. | +| pid | LONG | Process pid. | +| process_name | STRING | Process name. | +| uid | LONG | Android app UID. | +| shared_uid | BOOL | Whether the UID is shared by multiple packages. | +| user_id | LONG | Android user id for multi-user devices | +| user_type | STRING | Type of the Android user (e.g., HEADLESS, SECONDARY) | +| package_name | STRING | Name of the packages running in this process. | +| version_code | LONG | Package version code. | +| debuggable | LONG | Whether package is debuggable. | +| is_kernel_task | BOOL | Whether the task is kernel or not | + +#### Functions + +**android_is_kernel_task** + +Returns true if the process is a kernel task. +Returns BOOL: True for kernel tasks + +| Argument | Type | Description | +|---|---|---| +| upid | LONG | Queried process | + +### android.screen_state + +#### Views/Tables + +**android_screen_state** + +TABLE +Table of the screen state - on, off or doze (always on display). + +| Column | Type | Description | +|---|---|---| +| id | ID | ID. | +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration. | +| simple_screen_state | STRING | Simplified screen state: 'unknown', 'off', 'doze' (AoD) or 'on' | +| short_screen_state | STRING | Full screen state, adding VR and suspended-while-displaying states. | +| screen_state | STRING | Human-readable string. | + +### android.screenshots + +#### Views/Tables + +**android_screenshots** + +TABLE +Screenshot slices, used in perfetto UI. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Id of the screenshot slice. | +| ts | TIMESTAMP | Slice timestamp. | +| dur | DURATION | Slice duration, should be typically 0 since screeenshot slices are of instant type. | +| name | STRING | Slice name. | + +### android.services + +#### Views/Tables + +**android_service_bindings** + +TABLE +All service bindings from client app to server app. + +| Column | Type | Description | +|---|---|---| +| client_oom_score | LONG | OOM score of client process making the binding. | +| client_process | STRING | Name of client process making the binding. | +| client_thread | STRING | Name of client thread making the binding. | +| client_pid | LONG | Pid of client process making the binding. | +| client_tid | LONG | Tid of client process making the binding. | +| client_upid | JOINID(process.id) | Upid of client process making the binding. | +| client_utid | JOINID(thread.id) | Utid of client thread making the binding. | +| client_ts | TIMESTAMP | Timestamp the client process made the request. | +| client_dur | DURATION | Duration of the client binding request. | +| server_oom_score | LONG | OOM score of server process getting bound to. | +| server_process | STRING | Name of server process getting bound to | +| server_thread | STRING | Name of server thread getting bound to. | +| server_pid | LONG | Pid of server process getting bound to. | +| server_tid | LONG | Tid of server process getting bound to. | +| server_upid | JOINID(process.id) | Upid of server process getting bound to. | +| server_utid | JOINID(thread.id) | Utid of server process getting bound to. | +| server_ts | TIMESTAMP | Timestamp the server process got bound to. | +| server_dur | DURATION | Duration of the server process handling the binding. | +| token | STRING | Unique binder identifier for the Service binding. | +| act | STRING | Intent action name for the service binding. | +| cmp | STRING | Intent component name for the service binding. | +| flg | STRING | Intent flag for the service binding. | +| bind_seq | LONG | Monotonically increasing id for the service binding. | + +### android.slices + +#### Functions + +**android_standardize_slice_name** + +Some slice names have params in them. This functions removes them to make it +possible to aggregate by name. +Some examples are: + +- Lock/monitor contention slices. The name includes where the lock contention is in the code. That part is removed. +- DrawFrames/ooFrame. The name also includes the frame number. +- Apk/oat/dex loading: The name of the apk is removed Returns STRING: Simplified name. + +| Argument | Type | Description | +|---|---|---| +| name | STRING | The raw slice name. | + +### android.startup.startup_breakdowns + +#### Views/Tables + +**android_startup_opinionated_breakdown** + +TABLE +Blended thread state and slice breakdown blocking app startups. + +Each row blames a unique period during an app startup with a reason +derived from the slices and thread states on the main thread. + +Some helpful events to enables are binder transactions, ART, am and view. + +| Column | Type | Description | +|---|---|---| +| startup_id | JOINID(android_startups.startup_id) | Startup id. | +| slice_id | JOINID(slice.id) | Id of relevant slice blocking startup. | +| thread_state_id | JOINID(thread_state.id) | Id of thread_state blocking startup. | +| ts | TIMESTAMP | Timestamp of an exclusive interval during the app startup with a single latency reason. | +| dur | DURATION | Duration of an exclusive interval during the app startup with a single latency reason. | +| reason | STRING | Cause of delay during an exclusive interval of the app startup. | + +### android.startup.startups + +#### Views/Tables + +**android_startup_processes** + +TABLE +Maps a startup to the set of processes that handled the activity start. + +The vast majority of cases should be a single process. However it is +possible that the process dies during the activity startup and is respawned. + +| Column | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| upid | JOINID(process.id) | Upid of process on which activity started. | +| pid | LONG | Pid of process on which activity started. | +| startup_type | STRING | Type of the startup. | + +**android_startups** + +VIEW +All activity startups in the trace by startup id. +Populated by different scripts depending on the platform version/contents. + +| Column | Type | Description | +|---|---|---| +| startup_id | ID | Startup id. | +| ts | TIMESTAMP | Timestamp of startup start. | +| ts_end | LONG | Timestamp of startup end. | +| dur | DURATION | Startup duration. | +| package | STRING | Package name. | +| startup_type | STRING | Startup type. | + +**android_startup_threads** + +VIEW +Maps a startup to the set of threads on processes that handled the +activity start. + +| Column | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| ts | TIMESTAMP | Timestamp of start. | +| dur | DURATION | Duration of startup. | +| upid | JOINID(process.id) | Upid of process involved in startup. | +| pid | LONG | Pid if process involved in startup. | +| utid | JOINID(thread.id) | Utid of the thread. | +| tid | LONG | Tid of the thread. | +| thread_name | STRING | Name of the thread. | +| is_main_thread | BOOL | Thread is a main thread. | + +**android_thread_slices_for_all_startups** + +VIEW +All the slices for all startups in trace. + +Generally, this view should not be used. Instead, use one of the view functions related +to the startup slices which are created from this table. + +| Column | Type | Description | +|---|---|---| +| startup_ts | TIMESTAMP | Timestamp of startup. | +| startup_ts_end | LONG | Timestamp of startup end. | +| startup_id | LONG | Startup id. | +| utid | JOINID(thread.id) | UTID of thread with slice. | +| tid | LONG | Tid of thread. | +| thread_name | STRING | Name of thread. | +| is_main_thread | BOOL | Whether it is main thread. | +| arg_set_id | ARGSETID | Arg set id. | +| slice_id | JOINID(slice.id) | Slice id. | +| slice_name | STRING | Name of slice. | +| slice_ts | TIMESTAMP | Timestamp of slice start. | +| slice_dur | LONG | Slice duration. | + +**android_class_loading_for_startup** + +VIEW +A Perfetto view that lists matching slices for class loading during app startup. + +| Column | Type | Description | +|---|---|---| +| slice_id | JOINID(slice.id) | Id of the slice. | +| startup_id | LONG | Startup id. | +| slice_name | STRING | Name of the slice. | +| slice_ts | TIMESTAMP | Timestamp of start of the slice. | +| slice_dur | DURATION | Duration of the slice. | +| thread_name | STRING | Name of the thread with the slice. | +| tid | LONG | Tid of the thread with the slice. | +| arg_set_id | ARGSETID | Arg set id. | + +#### Functions + +**android_sum_dur_for_startup_and_slice** + +Returns duration of startup for slice name. + +Sums duration of all slices of startup with provided name. +Returns LONG: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +**android_sum_dur_on_main_thread_for_startup_and_slice** + +Returns duration of startup for slice name on main thread. + +Sums duration of all slices of startup with provided name only on main thread. +Returns LONG: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +#### Table Functions + +**android_slices_for_startup_and_slice_name** + +Given a startup id and GLOB for a slice name, returns matching slices with data. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Glob of the slice. | + +| Column | Type | Description | +|---|---|---| +| slice_id | JOINID(slice.id) | Id of the slice. | +| slice_name | STRING | Name of the slice. | +| slice_ts | TIMESTAMP | Timestamp of start of the slice. | +| slice_dur | DURATION | Duration of the slice. | +| thread_name | STRING | Name of the thread with the slice. | +| tid | LONG | Tid of the thread with the slice. | +| arg_set_id | ARGSETID | Arg set id. | + +**android_binder_transaction_slices_for_startup** + +Returns binder transaction slices for a given startup id with duration over threshold. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| threshold | DOUBLE | Only return slices with duration over threshold. | + +| Column | Type | Description | +|---|---|---| +| id | LONG | Slice id. | +| slice_dur | DURATION | Slice duration. | +| thread_name | STRING | Name of the thread with slice. | +| process | STRING | Name of the process with slice. | +| arg_set_id | ARGSETID | Arg set id. | +| is_main_thread | BOOL | Whether is main thread. | + +### android.startup.time_to_display + +#### Views/Tables + +**android_startup_time_to_display** + +TABLE +Startup metric defintions, which focus on the observable time range: +TTID - Time To Initial Display + +- +- end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display +- +- end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. + +| Column | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| time_to_initial_display | LONG | Time to initial display (TTID) | +| time_to_full_display | LONG | Time to full display (TTFD) | +| ttid_frame_id | LONG | `android_frames.frame_id` of frame for initial display | +| ttfd_frame_id | LONG | `android_frames.frame_id` of frame for full display | +| upid | JOINID(process.id) | `process.upid` of the startup | + +### android.statsd + +#### Views/Tables + +**android_statsd_atoms** + +VIEW +Statsd atoms. + +A subset of the slice table containing statsd atom instant events. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique identifier for this slice. | +| ts | TIMESTAMP | The timestamp at the start of the slice. | +| dur | DURATION | The duration of the slice. | +| arg_set_id | ARGSETID | The id of the argument set associated with this slice. | +| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| track_id | JOINID(track.id) | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | LONG | The depth of the slice in the current stack of slices. | +| parent_id | LONG | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| thread_ts | TIMESTAMP | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | + +### android.surfaceflinger + +#### Views/Tables + +**android_app_to_sf_frame_timeline_match** + +TABLE +Match the frame timeline on the app side with the frame timeline on the SF side. +In cases where there are multiple layers drawn, there would be separate frame timeline +slice for each of the layers. GROUP BY is used to deduplicate these rows. + +| Column | Type | Description | +|---|---|---| +| app_upid | JOINID(process.upid) | upid of the app. | +| app_vsync | LONG | vsync id of the app. | +| sf_upid | JOINID(process.upid) | upid of surfaceflinger process. | +| sf_vsync | LONG | vsync id for surfaceflinger. | + +### android.suspend + +#### Views/Tables + +**android_suspend_state** + +TABLE +Table of suspended and awake slices. + +Selects either the minimal or full ftrace source depending on what's +available, marks suspended periods, and complements them to give awake +periods. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| power_state | STRING | 'awake' or 'suspended' | + +### android.thread + +#### Functions + +**android_standardize_thread_name** + +Standardizes an Android thread name by extracting its core identifier to make it +possible to aggregate by name. + +Removes extra parts of a thread name, like identifiers, leaving only the main prefix. +Splits the name at ('-', '\[', ':' , ' '). + +Some Examples: +Given thread_name = "RenderThread-1\[123\]", +returns "RenderThread". + +Given thread_name = "binder:5543_E" +returns "binder". + +Given thread_name = "pool-3-thread-5", +returns "pool". + +Given thread_name = "MainThread", +returns "MainThread". +Returns STRING: Simplified name + +| Argument | Type | Description | +|---|---|---| +| thread_name | STRING | The full android thread name to be processed. | + +### android.user_list + +#### Views/Tables + +**android_user_list** + +VIEW +Contains information about Android users in the trace. + +This is populated by the `android.user_list` data-source which lives in +traced_probes and is available on default from 26Q2+ devices. + +> [!NOTE] +> **Note:** `users` here corresponds to Android users *not* Linux users. So this is not about Linux uids (which in Android correspons to different apps). + +| Column | Type | Description | +|---|---|---| +| android_user_id | LONG | The Android user id. Often useful to join with `android_process_metadata.user_id` | +| type | STRING | A string "enum" indicating the type of the user according to the system. Will be one for a few constant values e.g. HEADLESS, SECONDARY, GUEST. | + +### android.wakeups + +#### Views/Tables + +**android_wakeups** + +TABLE +Table of parsed wakeup / suspend failure events with suspend backoff. + +Certain wakeup events may have multiple causes. When this occurs we +split those causes into multiple rows in this table with the same ts +and raw_wakeup values. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| dur | DURATION | Duration for which we blame the wakeup for wakefulness. This is the suspend backoff duration if one exists, or the lesser of (5 seconds, time to next suspend event). | +| raw_wakeup | STRING | Original wakeup string from the kernel. | +| on_device_attribution | STRING | Wakeup attribution, as determined on device. May be absent. | +| type | STRING | One of 'normal' (device woke from sleep), 'abort_pending' (suspend failed due to a wakeup that was scheduled by a device during the suspend process), 'abort_last_active' (suspend failed, listing the last active device) or 'abort_other' (suspend failed for another reason). | +| item | STRING | Individual wakeup cause. Usually the name of the device that cause the wakeup, or the raw message in the 'abort_other' case. | +| suspend_quality | STRING | 'good' or 'bad'. 'bad' means failed or short such that suspend backoff is triggered. | +| backoff_state | STRING | 'new', 'continue' or NULL. Set if suspend backoff is triggered. | +| backoff_reason | STRING | 'short', 'failed' or NULL. Set if suspend backoff is triggered. | +| backoff_count | LONG | Number of times suspend backoff has occurred, or NULL. Set if suspend backoff is triggered. | +| backoff_millis | LONG | Next suspend backoff duration, or NULL. Set if suspend backoff is triggered. | + +### android.winscope.inputmethod + +#### Views/Tables + +**android_inputmethod_clients** + +VIEW +Android inputmethod clients state dumps (from android.inputmethod data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Dump id | +| ts | TIMESTAMP | Timestamp when the dump was triggered | +| arg_set_id | ARGSETID | Extra args parsed from the proto message | + +**android_inputmethod_manager_service** + +VIEW +Android inputmethod manager service state dumps (from android.inputmethod data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Dump id | +| ts | TIMESTAMP | Timestamp when the dump was triggered | +| arg_set_id | ARGSETID | Extra args parsed from the proto message | + +**android_inputmethod_service** + +VIEW +Android inputmethod service state dumps (from android.inputmethod data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Dump id | +| ts | TIMESTAMP | Timestamp when the dump was triggered | +| arg_set_id | ARGSETID | Extra args parsed from the proto message | + +### android.winscope.rect + +#### Views/Tables + +**android_winscope_rect** + +VIEW +Android Winscope rects. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Rect id | +| x | DOUBLE | x | +| y | DOUBLE | y | +| w | DOUBLE | w | +| h | DOUBLE | h | + +**android_winscope_transform** + +VIEW +Android Winscope transforms. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Transform id | +| dsdx | DOUBLE | dsdx | +| dtdx | DOUBLE | dtdx | +| tx | DOUBLE | tx | +| dtdy | DOUBLE | dtdy | +| dsdy | DOUBLE | dsdy | +| ty | DOUBLE | ty | + +**android_winscope_trace_rect** + +VIEW +Android Winscope trace rects. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Trace rect id | +| rect_id | LONG | Rect id | +| group_id | LONG | Group id | +| depth | LONG | Depth | +| is_spy | LONG | Is spy rect | +| is_visible | LONG | Is visible | +| opacity | DOUBLE | Opacity | +| transform_id | LONG | Transform id | +| border_width | LONG | Border width | +| border_color | DOUBLE | Border color | + +### android.winscope.surfaceflinger + +#### Views/Tables + +**android_surfaceflinger_transaction** + +VIEW +Android surfaceflinger transactions (from android.surfaceflinger.transactions data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Row id | +| snapshot_id | LONG | Snapshot id | +| arg_set_id | LONG | Arg set id | +| transaction_id | LONG | Transaction id | +| pid | LONG | PID | +| uid | LONG | UID | +| layer_id | LONG | Layer id | +| display_id | LONG | Display id | +| flags_id | LONG | Flags id | +| transaction_type | STRING | Transaction type | + +**android_surfaceflinger_transaction_flag** + +VIEW +Android surfaceflinger transaction flags. + +| Column | Type | Description | +|---|---|---| +| flags_id | LONG | Flags id | +| flag | STRING | Flag | + +**android_surfaceflinger_display** + +VIEW +Android surfaceflinger displays (from android.surfaceflinger.layers data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Id | +| snapshot_id | LONG | Snapshot id | +| is_on | LONG | Is on | +| is_virtual | LONG | Is virtual | +| trace_rect_id | LONG | Trace rect id | +| display_id | LONG | Display id | +| display_name | STRING | Display name | + +**android_winscope_fill_region** + +VIEW +Android surfaceflinger input rect fill regions (from android.surfaceflinger.layers data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Fill region id | +| trace_rect_id | LONG | Trace rect id | +| rect_id | LONG | Rect id | + +### android.winscope.transitions + +#### Views/Tables + +**android_window_manager_shell_transition_participants** + +VIEW +Android transition participants (from com.android.wm.shell.transition data source). + +| Column | Type | Description | +|---|---|---| +| transition_id | LONG | Transition id | +| layer_id | LONG | Layer participant | +| window_id | LONG | Window participant | + +**android_window_manager_shell_transition_protos** + +VIEW +Android transition protos (from com.android.wm.shell.transition data source). + +| Column | Type | Description | +|---|---|---| +| transition_id | LONG | Transition id | +| base64_proto_id | LONG | Base64 proto id | + +### android.winscope.viewcapture + +#### Views/Tables + +**android_viewcapture** + +VIEW +Android viewcapture (from android.viewcapture data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Snapshot id | +| ts | TIMESTAMP | Timestamp when snapshot was triggered | +| arg_set_id | ARGSETID | Extra args parsed from proto message | +| package_name | STRING | Package name deinterned from proto message | +| window_name | STRING | Window name deinterned from proto message | + +**android_viewcapture_view** + +VIEW +Android viewcapture view (from android.viewcapture data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Row id | +| snapshot_id | LONG | Snapshot id | +| arg_set_id | ARGSETID | Extra args parsed from proto message | +| node_id | LONG | Id from proto message | +| hashcode | LONG | Hashcode from proto message | +| is_visible | LONG | Is visible computed from proto message and view position | +| parent_id | LONG | Parent id from proto message | +| view_id | STRING | View id deinterned from proto message | +| class_name | STRING | Class name deinterned from proto message | +| trace_rect_id | LONG | Trace rect id | + +### android.winscope.windowmanager + +#### Views/Tables + +**android_windowmanager** + +VIEW +Android WindowManager (from android.windowmanager data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Snapshot id | +| ts | TIMESTAMP | Timestamp when the snapshot was triggered | +| arg_set_id | ARGSETID | Extra args parsed from the proto message | +| base64_proto_id | LONG | Raw proto message | +| focused_display_id | LONG | Focused display id for this snapshot | +| has_invalid_elapsed_ts | BOOL | Indicates whether snapshot was recorded without elapsed timestamp | + +**android_windowmanager_windowcontainer** + +VIEW +Android WindowManager WindowContainer (from android.windowmanager data source). + +| Column | Type | Description | +|---|---|---| +| id | LONG | Row id | +| snapshot_id | LONG | Snapshot id | +| arg_set_id | ARGSETID | Extra args parsed from the proto message | +| base64_proto_id | LONG | Raw proto message | +| title | STRING | The window container's title | +| token | LONG | The window container's token | +| parent_token | LONG | The parent window container's token | +| child_index | LONG | The index of this window container within the parent's children | +| is_visible | BOOL | The window container visibility | +| window_rect_id | LONG | The rect corresponding to this window container | +| container_type | STRING | The window container type e.g. DisplayContent, TaskFragment | +| name_override | STRING | Optional name override for some container types | + +## Package: v8 + +### v8.jit + +#### Views/Tables + +**v8_isolate** + +VIEW +A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8 +engine. + +| Column | Type | Description | +|---|---|---| +| v8_isolate_id | LONG | Unique V8 isolate id. | +| upid | JOINID(process.id) | Process the isolate was created in. | +| internal_isolate_id | LONG | Internal id used by the v8 engine. Unique in a process. | +| embedded_blob_code_start_address | LONG | Absolute start address of the embedded code blob. | +| embedded_blob_code_size | LONG | Size in bytes of the embedded code blob. | +| code_range_base_address | LONG | Base address of the code range if the isolate defines one. | +| code_range_size | LONG | Size of a code range if the isolate defines one. | +| shared_code_range | LONG | Whether the code range for this Isolate is shared with others in the same process. There is at max one such shared code range per process. | +| embedded_blob_code_copy_start_address | LONG | Used when short builtin calls are enabled, where embedded builtins are copied into the CodeRange so calls can be nearer. | + +**v8_js_script** + +VIEW +Represents a script that was compiled to generate code. Some V8 code is +generated out of scripts and will reference a V8Script other types of code +will not (e.g. builtins). + +| Column | Type | Description | +|---|---|---| +| v8_js_script_id | LONG | Unique V8 JS script id. | +| v8_isolate_id | LONG | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | LONG | Script id used by the V8 engine. | +| script_type | STRING | Script type. | +| name | STRING | Script name. | +| source | STRING | Actual contents of the script. | + +**v8_wasm_script** + +VIEW +Represents one WASM script. + +| Column | Type | Description | +|---|---|---| +| v8_wasm_script_id | LONG | Unique V8 WASM script id. | +| v8_isolate_id | LONG | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | LONG | Script id used by the V8 engine. | +| url | STRING | URL of the source. | +| wire_bytes | BYTES | Raw wire bytes of the script. | +| source | STRING | Actual source code of the script. | + +**v8_js_function** + +VIEW +Represents a v8 Javascript function. + +| Column | Type | Description | +|---|---|---| +| v8_js_function_id | LONG | Unique V8 JS function id. | +| name | STRING | Function name. | +| v8_js_script_id | LONG | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | +| is_toplevel | BOOL | Whether this function represents the top level script. | +| kind | STRING | Function kind (e.g. regular function or constructor). | +| line | LONG | Line in script where function is defined. Starts at 1. | +| col | LONG | Column in script where function is defined. Starts at 1. | + +## Package: intervals + +### intervals.intersect + +#### Macros + +**interval_self_intersect** + +Given a list of intervals (ts, dur), this macro generates a list of interval +end points as well as the intervals that intersect with those points. + +e.g. input (10, 20), (20, 25) + +`10 30 +A |---| +B|---| +20 45` would generate the output: + +`ts,dur,group_id,id,interval_ends_at_ts +10,10,1,A,0 +20,10,2,A,0 +20,10,2,B,0 +30,15,3,A,1 +30,15,3,B,0 +45,0,4,B,1` Runtime is O(n log n + m), where n is the number of intervals and m +is the size of the output. +Returns: TableOrSubquery, + +| Argument | Type | Description | +|---|---|---| +| intervals | TableOrSubquery | Table or subquery containing interval data. | + +### intervals.overlap + +#### Macros + +**intervals_overlap_count** + +Compute the distribution of the overlap of the given intervals over time. + +Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) +counter, with the value corresponding to the number of intervals that overlap +the given timestamp and interval until the next timestamp. +Returns: TableOrSubquery, The returned table has the schema (ts TIMESTAMP, value LONG). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. + +| Argument | Type | Description | +|---|---|---| +| segments | TableOrSubquery | Table or subquery containing interval data. | +| ts_column | ColumnName | Column containing interval starts (usually `ts`). | +| dur_column | ColumnName | Column containing interval durations (usually `dur`). | + +**intervals_overlap_count_by_group** + +Compute the distribution of the overlap of the given intervals over time from +slices in a same group. + +Each interval is a (ts, dur, group) triple and the overlap represented as a +(ts, value, group) counter, with the value corresponding to the number of +intervals that belong to the same group and overlap the given timestamp and +interval until the next timestamp. +Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32, group_name) where the type of group_name is the same as that in \|segments\|. \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. \|group_name\| is the name of a group used for the overlap calculation. + +| Argument | Type | Description | +|---|---|---| +| segments | TableOrSubquery | Table or subquery containing interval data. | +| ts_column | ColumnName | Column containing interval starts (usually `ts`). | +| dur_column | ColumnName | Column containing interval durations (usually `dur`). | +| group_column | ColumnName | Column containing group name for grouping. | + +**interval_merge_overlapping** + +Merge intervals when they overlap to generate a minimum covering set of +intervals with no overlap. The intervals are closed (contain both endpoints) +and we consider two intervals overlapping +(a) the intervals overlap or +(b) if the end point of one interval is within epsilon of the start point +of the other. +Returns: TableOrSubquery, + +| Argument | Type | Description | +|---|---|---| +| intervals | TableOrSubquery | Table or subquery containing interval data. | +| epsilon | Expr | Constant expression for a tolerance in testing overlap (usually `0`) | + +**interval_merge_overlapping_partitioned** + +Merge overlapping intervals within each partition group to generate a minimum +covering set of intervals with no overlap within each partition. + +For each partition, this macro merges overlapping intervals into +non-overlapping intervals. The result contains intervals where at least +one input interval is active. + +For example, with partition 'A': +Input: (ts=1, dur=10), (ts=5, dur=12) +Output: (ts=1, dur=16) +Returns: TableOrSubquery, The returned table has the schema (ts TIMESTAMP, dur DURATION, partitions). \|ts\| is the start of the merged interval. \|dur\| is the duration of the merged interval. \|partitions\| is all of the columns in partition_columns. + +| Argument | Type | Description | +|---|---|---| +| intervals | TableOrSubquery | Table or subquery containing interval data. | +| partition_columns | ColumnNameList | Column name for partition grouping. | + +## Package: counters + +### counters.intervals + +#### Macros + +**counter_leading_intervals** + +For a given counter timeline (e.g. a single counter track), returns +intervals of time where the counter has the same value. For every run +of identical values, this macro will return a row for the first one, +the last one, and a row merging all other ones. This to to facilitate +construction of counters from delta_values. + +Intervals are computed in a "forward-looking" way. That is, if a counter +changes value at some timestamp, it's assumed it *just* reached that +value and it should continue to have that value until the next +value change. The final value is assumed to hold until the very end of +the trace. + +For example, suppose we have the following data: + +`ts=0, value=10, track_id=1 +ts=0, value=10, track_id=2 +ts=10, value=10, track_id=1 +ts=10, value=20, track_id=2 +ts=20, value=30, track_id=1 +[end of trace at ts = 40]` Then this macro will generate the following intervals: + +`ts=0, dur=10, value=10, track_id=1 +ts=10, dur=10, value=10, track_id=1 +ts=20, dur=10, value=30, track_id=1 +ts=0, dur=10, value=10, track_id=2 +ts=10, dur=30, value=20, track_id=2`Returns: TableOrSubquery, Table with the schema: id LONG, As passed in ts TIMESTAMP, As passed in dur DURATION, Difference to the timestamp for the leading row. track_id JOINID(track.id), As passed in value DOUBLE, As passed in next_value DOUBLE, Value for the leading row. delta_value DOUBLE Delta to the *lagging* row - note that this is not the same thing as (next_value - value). + +| Argument | Type | Description | +|---|---|---| +| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | + +## Package: linux + +### linux.block_io + +#### Views/Tables + +**linux_active_block_io_operations_by_device** + +VIEW +View tracking the number of IO operations remaining in the kernel IO queue or +a block device + +| Column | Type | Description | +|---|---|---| +| ts | LONG | timestamp when block_io_start or block_io_done happened | +| ops_in_queue_or_device | LONG | the number of IO operations in the kernel queue or the device | +| dev | LONG | the device processing the IO operations | + +#### Functions + +**linux_device_major_id** + +Returns LONG: 12 bits major id + +| Argument | Type | Description | +|---|---|---| +| dev | LONG | device id (userland dev_t value) | + +**linux_device_minor_id** + +Returns LONG: 20 bits minor id + +| Argument | Type | Description | +|---|---|---| +| dev | LONG | device id (userland dev_t value) | + +### linux.cpu.frequency + +#### Views/Tables + +**cpu_frequency_counters** + +TABLE +Counter information for each frequency change for each CPU. Finds each time +region where a CPU frequency is constant. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Counter id. | +| track_id | JOINID(track.id) | Joinable with 'counter_track.id'. | +| ts | TIMESTAMP | Starting timestamp of the counter | +| dur | DURATION | Duration in which counter is constant and frequency doesn't change. | +| freq | LONG | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | +| ucpu | LONG | Unique CPU id. | +| cpu | LONG | CPU that corresponds to this counter. | + +### linux.cpu.idle + +#### Views/Tables + +**cpu_idle_counters** + +TABLE +Counter information for each idle state change for each CPU. Finds each time +region where a CPU idle state is constant. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Counter id. | +| track_id | JOINID(track.id) | Joinable with 'counter_track.id'. | +| ts | TIMESTAMP | Starting timestamp of the counter. | +| dur | DURATION | Duration in which the counter is contant and idle state doesn't change. | +| idle | LONG | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | +| cpu | LONG | CPU that corresponds to this counter. | + +### linux.cpu.idle_stats + +#### Views/Tables + +**cpu_idle_stats** + +TABLE +Aggregates cpu idle statistics per core. + +| Column | Type | Description | +|---|---|---| +| cpu | LONG | CPU core number. | +| state | LONG | CPU idle state (C-states). | +| count | LONG | The count of entering idle state. | +| dur | DURATION | Total CPU core idle state duration. | +| avg_dur | DURATION | Average CPU core idle state duration. | +| idle_percent | DOUBLE | Idle state percentage of non suspend time (C-states + P-states). | + +### linux.cpu.idle_time_in_state + +#### Views/Tables + +**linux_per_cpu_idle_time_in_state_counters** + +TABLE +Percentage counter information for sysfs cpuidle states. +For each state per cpu, report the incremental time spent in one state, +divided by time spent in all states, between two timestamps. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| machine_id | LONG | The machine this residency is calculated for. | +| state | STRING | State name. | +| cpu | LONG | CPU. | +| idle_percentage | DOUBLE | Percentage of time this cpu spent in this state. | +| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | +| time_slice | LONG | Time this cpu spent in any state, in microseconds. | + +**linux_cpu_idle_time_in_state_counters** + +TABLE +Percentage counter information for sysfs cpuidle states. +For each state across all CPUs, report the incremental time spent in one +state, divided by time spent in all states, between two timestamps. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp. | +| machine_id | LONG | The machine this residency is calculated for. | +| state | STRING | State name. | +| idle_percentage | DOUBLE | Percentage of time all CPUS spent in this state. | +| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | +| time_slice | LONG | Time all CPUS spent in any state, in microseconds. | + +### linux.cpu.utilization.process + +#### Views/Tables + +**cpu_cycles_per_process** + +TABLE +Aggregated CPU statistics for each process. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Unique process id | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +#### Table Functions + +**cpu_process_utilization_per_period** + +Returns a table of process utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. + +| Argument | Type | Description | +|---|---|---| +| interval | LONG | Length of the period on which utilization should be averaged. | +| upid | JOINID(process.id) | Upid of the process. | + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_process_utilization_per_second** + +Returns a table of process utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. + +| Argument | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of the process. | + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_cycles_per_process_in_interval** + +Aggregated CPU statistics for each process in a provided interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Unique process id. | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| awake_runtime | DURATION | Total runtime duration, while 'awake' (CPUs not suspended). | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +**cpu_process_utilization_in_interval** + +Returns a table with process utilization over a given interval. + +Utilization is computed as runtime over the duration of the interval, aggregated by UPID. +Utilization can be normalized (divide by number of cpus) or unnormalized. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Unique process id. | +| process_name | STRING | The name of the process. | +| awake_dur | LONG | Total runtime of all processes with this UPID, while 'awake' (CPUs not suspended). | +| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | +| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | + +### linux.cpu.utilization.slice + +#### Views/Tables + +**cpu_cycles_per_thread_slice** + +TABLE +CPU cycles per each slice. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Id of a slice. | +| name | STRING | Name of the slice. | +| utid | JOINID(thread.id) | Id of the thread the slice is running on. | +| thread_name | STRING | Name of the thread. | +| upid | JOINID(process.id) | Id of the process the slice is running on. | +| process_name | STRING | Name of the process. | +| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +#### Table Functions + +**cpu_cycles_per_thread_slice_in_interval** + +CPU cycles per each slice in interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | DURATION | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Thread slice. | +| name | STRING | Name of the slice. | +| utid | JOINID(thread.id) | Thread the slice is running on. | +| thread_name | STRING | Name of the thread. | +| upid | JOINID(process.id) | Process the slice is running on. | +| process_name | STRING | Name of the process. | +| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +### linux.cpu.utilization.system + +#### Views/Tables + +**cpu_utilization_per_second** + +TABLE +Table with system utilization per second. +Utilization is calculated by sum of average utilization of each CPU every +second. For this reason first and last second might have lower then real +utilization. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_cycles** + +TABLE +Aggregated CPU statistics for whole trace. Results in only one row. + +| Column | Type | Description | +|---|---|---| +| millicycles | LONG | Sum of CPU millicycles. | +| megacycles | LONG | Sum of CPU megacycles. | +| runtime | DURATION | Total runtime of all threads running on all CPUs. | +| min_freq | LONG | Minimum CPU frequency in kHz. | +| max_freq | LONG | Maximum CPU frequency in kHz. | +| avg_freq | LONG | Average CPU frequency in kHz. | + +**cpu_cycles_per_cpu** + +TABLE +Aggregated CPU statistics for each CPU. + +| Column | Type | Description | +|---|---|---| +| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | +| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | LONG | Sum of CPU millicycles. | +| megacycles | LONG | Sum of CPU megacycles. | +| runtime | DURATION | Total runtime of all threads running on CPU. | +| min_freq | LONG | Minimum CPU frequency in kHz. | +| max_freq | LONG | Maximum CPU frequency in kHz. | +| avg_freq | LONG | Average CPU frequency in kHz. | + +#### Table Functions + +**cpu_utilization_per_period** + +Returns a table of system utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. + +| Argument | Type | Description | +|---|---|---| +| interval | LONG | Length of the period on which utilization should be averaged. | + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_cycles_in_interval** + +Aggregated CPU statistics in a provided interval. Results in one row. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| millicycles | LONG | Sum of CPU millicycles. | +| megacycles | LONG | Sum of CPU megacycles. | +| runtime | DURATION | Total runtime of all threads running on all CPUs. | +| awake_runtime | DURATION | Total runtime of all threads running on all CPUs, while 'awake' (CPUs not suspended). | +| min_freq | LONG | Minimum CPU frequency in kHz. | +| max_freq | LONG | Maximum CPU frequency in kHz. | +| avg_freq | LONG | Average CPU frequency in kHz. | + +**cpu_utilization_in_interval** + +Returns a table of CPU utilization over a given interval. + +Utilization is computed as runtime over the duration of the interval. +Utilization can be normalized (divide by number of cores) or unnormalized. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| awake_dur | LONG | Total runtime of all threads running on all CPUs, while 'awake' (CPUs not suspended). | +| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | +| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | + +**cpu_cycles_per_cpu_in_interval** + +Aggregated CPU statistics for each CPU in a provided interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | +| cpu | LONG | CPU number. | +| millicycles | LONG | Sum of CPU millicycles. | +| megacycles | LONG | Sum of CPU megacycles. | +| runtime | DURATION | Total runtime of all threads running on CPU. | +| min_freq | LONG | Minimum CPU frequency in kHz. | +| max_freq | LONG | Maximum CPU frequency in kHz. | +| avg_freq | LONG | Average CPU frequency in kHz. | + +### linux.cpu.utilization.thread + +#### Views/Tables + +**cpu_cycles_per_thread** + +TABLE +Aggregated CPU statistics for each thread. + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Thread | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +#### Table Functions + +**cpu_thread_utilization_per_period** + +Returns a table of thread utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. + +| Argument | Type | Description | +|---|---|---| +| interval | LONG | Length of the period on which utilization should be averaged. | +| utid | JOINID(thread.id) | Utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_thread_utilization_per_second** + +Returns a table of thread utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. + +| Argument | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +**cpu_cycles_per_thread_in_interval** + +Aggregated CPU statistics for each thread in a provided interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Thread with CPU cycles and frequency statistics. | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| awake_runtime | DURATION | Total runtime duration, while 'awake' (CPUs not suspended). | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +**cpu_thread_utilization_in_interval** + +Returns a table of thread utilization over a given interval. + +Utilization is computed as runtime over the duration of the interval, aggregated by UTID. +Utilization can be normalized (divide by number of CPUs) or unnormalized. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Unique process id. | +| utid | JOINID(thread.id) | Unique thread id. | +| thread_name | STRING | The name of the thread | +| awake_dur | LONG | Total runtime of all threads with this UTID, while 'awake' (CPUs not suspended). | +| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | +| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | + +### linux.cpu.utilization.thread_cpu + +#### Views/Tables + +**cpu_cycles_per_thread_per_cpu** + +TABLE +Aggregated CPU statistics for each thread per CPU combination. +To operate properly this requires sched/sched_switch and power/cpu_frequency +ftrace events to be present in the trace. + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Thread | +| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | +| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +#### Table Functions + +**cpu_cycles_per_thread_per_cpu_in_interval** + +Aggregated CPU statistics for each thread per CPU combination in a provided interval. +To operate properly this requires sched/sched_switch and power/cpu_frequency +ftrace events to be present in the trace. +Warning: this query is expensive and might take a long time to execute when joined +with a large number of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Start of the interval. | +| dur | LONG | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Thread with CPU cycles and frequency statistics. | +| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | +| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | LONG | Sum of CPU millicycles | +| megacycles | LONG | Sum of CPU megacycles | +| runtime | DURATION | Total runtime duration | +| min_freq | LONG | Minimum CPU frequency in kHz | +| max_freq | LONG | Maximum CPU frequency in kHz | +| avg_freq | LONG | Average CPU frequency in kHz | + +### linux.devfreq + +#### Views/Tables + +**linux_devfreq_dsu_counter** + +TABLE +ARM DSU device frequency counters. This table will only be populated on +traces collected with "devfreq/devfreq_frequency" ftrace event enabled, +and from ARM devices with the DSU (DynamIQ Shared Unit) hardware. + +| Column | Type | Description | +|---|---|---| +| id | LONG | Unique identifier for this counter. | +| ts | TIMESTAMP | Starting timestamp of the counter. | +| dur | DURATION | Duration in which counter is constant and frequency doesn't chamge. | +| dsu_freq | LONG | Frequency in kHz of the device that corresponds to the counter. | + +### linux.irqs + +#### Views/Tables + +**linux_hard_irqs** + +VIEW +All hard IRQs of the trace represented as slices. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Starting timestamp of this IRQ. | +| dur | DURATION | Duration of this IRQ. | +| name | STRING | The name of the IRQ. | +| id | JOINID(slice.id) | The id of the IRQ. | +| parent_id | JOINID(slice.id) | The id of this IRQ's parent IRQ (i.e. the IRQ that this IRQ preempted). | + +**linux_soft_irqs** + +VIEW +All soft IRQs of the trace represented as slices. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Starting timestamp of this IRQ. | +| dur | DURATION | Duration of this IRQ. | +| name | STRING | The name of the IRQ. | +| id | JOINID(slice.id) | The id of the IRQ. | + +**linux_irqs** + +VIEW +All IRQs, including hard and soft IRQs, of the trace represented as slices. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Starting timestamp of this IRQ. | +| dur | DURATION | Duration of this IRQ. | +| name | STRING | The name of the IRQ. | +| id | JOINID(slice.id) | The id of the IRQ. | +| parent_id | JOINID(slice.id) | The id of this IRQ's parent IRQ (i.e. the IRQ that this IRQ preempted). | +| is_soft_irq | BOOL | Flag indicating if IRQ is soft IRQ | + +### linux.memory.high_watermark + +#### Views/Tables + +**memory_rss_high_watermark_per_process** + +VIEW +For each process fetches the memory high watermark until or during +timestamp. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| upid | JOINID(process.id) | Upid of the process | +| pid | LONG | Pid of the process | +| process_name | STRING | Name of the process | +| rss_high_watermark | LONG | Maximum `rss` value until now | + +### linux.memory.process + +#### Views/Tables + +**memory_rss_and_swap_per_process** + +VIEW +Memory metrics timeline for each process. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp | +| dur | DURATION | Duration | +| upid | JOINID(process.id) | Upid of the process | +| pid | LONG | Pid of the process | +| process_name | STRING | Name of the process | +| anon_rss | LONG | Anon RSS counter value | +| file_rss | LONG | File RSS counter value | +| shmem_rss | LONG | Shared memory RSS counter value | +| rss | LONG | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | LONG | Swap counter value | +| anon_rss_and_swap | LONG | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | LONG | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +### linux.perf.samples + +#### Views/Tables + +**linux_perf_samples_summary_tree** + +TABLE +Table summarising the callstacks captured during all +perf samples in the trace. + +Specifically, this table returns a tree containing all +the callstacks seen during the trace with `self_count` +equal to the number of samples with that frame as the +leaf and `cumulative_count` equal to the number of +samples with the frame anywhere in the tree. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | LONG | The id of the parent callstack for this callstack. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | LONG | The line number in the file the function is located at. | +| self_count | LONG | The number of samples with this function as the leaf frame. | +| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | + +### linux.perf.spe + +#### Views/Tables + +**linux_perf_spe_record** + +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestap when the operation was sampled | +| utid | JOINID(thread.id) | Thread the operation executed in | +| exception_level | STRING | Exception level the instruction was executed in | +| instruction_frame_id | LONG | Instruction virtual address | +| operation | STRING | Type of operation sampled | +| data_virtual_address | LONG | The virtual address accessed by the operation (0 if no memory access was performed) | +| data_physical_address | LONG | The physical address accessed by the operation (0 if no memory access was performed) | +| total_latency | LONG | Cycle count from the operation being dispatched for issue to the operation being complete. | +| issue_latency | LONG | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | +| translation_latency | LONG | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | +| data_source | STRING | Where the data returned for a load operation was sourced | +| exception_gen | BOOL | Operation generated an exception | +| retired | BOOL | Operation architecturally retired | +| l1d_access | BOOL | Operation caused a level 1 data cache access | +| l1d_refill | BOOL | Operation caused a level 1 data cache refill | +| tlb_access | BOOL | Operation caused a TLB access | +| tlb_refill | BOOL | Operation caused a TLB refill involving at least one translation table walk | +| not_taken | BOOL | Conditional instruction failed its condition code check | +| mispred | BOOL | Whether a branch caused a correction to the predicted program flow | +| llc_access | BOOL | Operation caused a last level data or unified cache access | +| llc_refill | BOOL | Whether the operation could not be completed by the last level data cache (or any above) | +| remote_access | BOOL | Operation caused an access to another socket in a multi-socket system | +| alignment | BOOL | Operation that incurred additional latency due to the alignment of the address and the size of the data being accessed | +| tme_transaction | BOOL | Whether the operation executed in transactional state | +| sve_partial_pred | BOOL | SVE or SME operation with at least one false element in the governing predicate(s) | +| sve_empty_pred | BOOL | SVE or SME operation with no true element in the governing predicate(s) | +| l2d_access | BOOL | Whether a load operation caused a cache access to at least the level 2 data or unified cache | +| l2d_hit | BOOL | Whether a load operation accessed and missed the level 2 data or unified cache. Not set for accesses that are satisfied from refilling data of a previous miss | +| cache_data_modified | BOOL | Whether a load operation accessed modified data in a cache | +| recenty_fetched | BOOL | Wheter a load operation hit a recently fetched line in a cache | +| data_snooped | BOOL | Whether a load operation snooped data from a cache outside the cache hierarchy of this core | + +### linux.threads + +#### Views/Tables + +**linux_kernel_threads** + +TABLE +All kernel threads of the trace. As kernel threads are processes, provides +also process data. + +| Column | Type | Description | +|---|---|---| +| upid | JOINID(process.id) | Upid of kernel thread. Alias of | +| utid | JOINID(thread.id) | Utid of kernel thread. Alias of | +| pid | LONG | Pid of kernel thread. Alias of | +| tid | LONG | Tid of kernel thread. Alias of | +| process_name | STRING | Name of kernel process. Alias of | +| thread_name | STRING | Name of kernel thread. Alias of | +| machine_id | LONG | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of | + +## Package: pixel + +### pixel.camera + +#### Views/Tables + +**pixel_camera_frames** + +TABLE +Break down camera Camera graph execution slices per node, port group, and frame. +This table extracts key identifiers from Camera graph execution slice names and +provides timing information for each processing stage. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Unique identifier for this slice. | +| ts | TIMESTAMP | Start timestamp of the slice. | +| dur | DURATION | Duration of the slice execution. | +| track_id | JOINID(track.id) | Track ID for this slice. | +| utid | JOINID(thread.id) | Thread ID (utid) executing this slice. | +| thread_name | STRING | Name of the thread executing this slice. | +| node | STRING | Name of the processing node in the Camera graph. | +| port_group | STRING | Port group name for the node. | +| frame_number | LONG | Frame number being processed. | +| cam_id | LONG | Camera ID associated with this slice. | + +## Package: slices + +### slices.cpu_time + +#### Views/Tables + +**thread_slice_cpu_time** + +TABLE +Time each thread slice spent running on CPU. +Requires scheduling data to be available in the trace. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Slice. | +| name | STRING | Name of the slice. | +| utid | JOINID(thread.id) | Id of the thread the slice is running on. | +| thread_name | STRING | Name of the thread. | +| upid | JOINID(process.id) | Id of the process the slice is running on. | +| process_name | STRING | Name of the process. | +| cpu_time | LONG | Duration of the time the slice was running. | + +**thread_slice_cpu_cycles** + +VIEW +CPU cycles per each slice. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Id of a slice. | +| name | STRING | Name of the slice. | +| utid | JOINID(thread.id) | Id of the thread the slice is running on. | +| thread_name | STRING | Name of the thread. | +| upid | JOINID(process.id) | Id of the process the slice is running on. | +| process_name | STRING | Name of the process. | +| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +### slices.self_dur + +#### Views/Tables + +**slice_self_dur** + +TABLE +For every slice in the `slice` table, computes the "self-duration": the time +spent in the slice but *not* spent in any child slices. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | The id of the slice. | +| self_dur | DURATION | The self duration for the slice: the time spent in the slice but not any child slices. | + +### slices.stack + +#### Views/Tables + +**slice_with_stack_id** + +VIEW +View that provides stack_id and parent_stack_id for all slices by computing +them on-demand. + +> [!NOTE] +> **Note:** This view computes stack hashes on-demand, which may be slower than the previous C++ implementation. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Slice id. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | +| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | +| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | +| stack_id | LONG | A unique identifier obtained from the names and categories of all slices in this stack. Computed on-demand. | +| parent_stack_id | LONG | The stack_id for the parent of this slice. 0 if there is no parent. | + +#### Table Functions + +**ancestor_slice_by_stack** + +Returns all slices that have the given stack_id, along with their ancestors. + +The stack_id can be obtained from the slice_with_stack_id view. + +| Argument | Type | Description | +|---|---|---| +| stack_hash | LONG | The stack hash to search for. | + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Slice id. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | + +**descendant_slice_by_stack** + +Returns all slices that have the given stack_id, along with their descendants. + +The stack_id can be obtained from the slice_with_stack_id view. + +| Argument | Type | Description | +|---|---|---| +| stack_hash | LONG | The stack hash to search for. | + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Slice id. | +| ts | TIMESTAMP | Alias of `slice.ts`. | +| dur | DURATION | Alias of `slice.dur`. | +| track_id | JOINID(track.id) | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | LONG | Alias of `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | + +### slices.time_in_state + +#### Views/Tables + +**thread_slice_time_in_state** + +TABLE +For each thread slice, returns the sum of the time it spent in various +scheduling states. + +Requires scheduling data to be available in the trace. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Thread slice. | +| name | STRING | Name of the slice. | +| utid | JOINID(thread.id) | Thread the slice is running on. | +| thread_name | STRING | Name of the thread. | +| upid | JOINID(process.id) | Id of the process the slice is running on. | +| process_name | STRING | Name of the process. | +| state | STRING | The scheduling state (from the `thread_state` table). Use the `sched_state_to_human_readable_string` function in the `sched` package to get full name. | +| io_wait | BOOL | If the `state` is uninterruptible sleep, `io_wait` indicates if it was an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if we cannot tell if it was an IO sleep or not. Only available on Android when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | +| blocked_function | STRING | If in uninterruptible sleep (D), the kernel function on which was blocked. Only available on userdebug Android builds when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | +| dur | DURATION | The duration of time the threads slice spent for each (state, io_wait, blocked_function) tuple. | + +### slices.with_context + +#### Views/Tables + +**thread_slice** + +VIEW +All thread slices with data about thread, thread track and process. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Slice | +| ts | TIMESTAMP | Alias for `slice.ts`. | +| dur | DURATION | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | JOINID(track.id) | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `thread_track.name`. | +| thread_name | STRING | Alias for `thread.name`. | +| utid | JOINID(thread.id) | Alias for `thread.utid`. | +| tid | LONG | Alias for `thread.tid`. | +| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | +| process_name | STRING | Alias for `process.name`. | +| upid | JOINID(process.id) | Alias for `process.upid`. | +| pid | LONG | Alias for `process.pid`. | +| depth | LONG | Alias for `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias for `slice.thread_ts`. | +| thread_dur | LONG | Alias for `slice.thread_dur`. | + +**process_slice** + +VIEW +All process slices with data about process track and process. + +| Column | Type | Description | +|---|---|---| +| id | ID(slice.id) | Slice | +| ts | TIMESTAMP | Alias for `slice.ts`. | +| dur | DURATION | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | JOINID(track.id) | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `process_track.name`. | +| process_name | STRING | Alias for `process.name`. | +| upid | JOINID(process.id) | Alias for `process.upid`. | +| pid | LONG | Alias for `process.pid`. | +| depth | LONG | Alias for `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | +| thread_ts | TIMESTAMP | Alias for `slice.thread_ts`. | +| thread_dur | LONG | Alias for `slice.thread_dur`. | + +**thread_or_process_slice** + +VIEW +All the slices in the trace associated to a thread or a process along +with contextual information about them (e.g. thread name, process name, tid etc). + +| Column | Type | Description | +|---|---|---| +| id | JOINID(slice.id) | Slice | +| ts | TIMESTAMP | Alias for `slice.ts`. | +| dur | DURATION | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | JOINID(track.id) | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `track.name`. | +| thread_name | STRING | Alias for `thread.name`. | +| utid | JOINID(thread.id) | Alias for `thread.utid`. | +| tid | LONG | Alias for `thread.tid` | +| process_name | STRING | Alias for `process.name`. | +| upid | JOINID(process.id) | Alias for `process.upid`. | +| pid | LONG | Alias for `process.pid`. | +| depth | LONG | Alias for `slice.depth`. | +| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | +| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | + +## Package: appleos + +### appleos.instruments.samples + +#### Views/Tables + +**appleos_instruments_samples_summary_tree** + +TABLE +Table summarising the callstacks captured during all +instruments samples in the trace. + +Specifically, this table returns a tree containing all +the callstacks seen during the trace with `self_count` +equal to the number of samples with that frame as the +leaf and `cumulative_count` equal to the number of +samples with the frame anywhere in the tree. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | LONG | The id of the parent callstack for this callstack. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, or JIT. | +| source_file | STRING | The name of the file containing the function. | +| line_number | LONG | The line number in the file the function is located at. | +| self_count | LONG | The number of samples with this function as the leaf frame. | +| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | + +## Package: graphs + +### graphs.dominator_tree + +#### Macros + +**graph_dominator_tree** + +Given a table containing a directed flow-graph and an entry node, computes +the "dominator tree" for the graph. See \[1\] for an explanation of what a +dominator tree is. + +\[1\] + +Example usage on traces containing heap graphs: + +\`CREATE PERFETTO VIEW dominator_compatible_heap_graph AS +-- Extract the edges from the heap graph which correspond to references +-- between objects. +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +JOIN heap_graph_object owner on heap_graph_reference.owner_id = owner.id +WHERE owned_id IS NOT NULL AND owner.reachable +UNION ALL +-- Since a Java heap graph is a "forest" structure, we need to add a dummy +-- "root" node which connects all the roots of the forest into a single +-- connected component. +SELECT +(SELECT max(id) + 1 FROM heap_graph_object) as source_node_id, +id +FROM heap_graph_object +WHERE root_type IS NOT NULL; + +SELECT \* +FROM graph_dominator_tree!( +dominator_compatible_heap_graph, +(SELECT max(id) + 1 FROM heap_graph_object) +);\`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, dominator_node_id LONG). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with | +| root_node_id | Expr | The entry node to | + +### graphs.partition + +#### Macros + +**tree_structural_partition_by_group** + +Partitions a tree into a forest of trees based on a given grouping key +in a structure-preserving way. + +Specifically, for each tree in the output forest, all the nodes in that tree +have the same ancestors and descendants as in the original tree *iff* that +ancestor/descendent belonged to the same group. + +Example: +Input + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 4 | 1 | + +Or as a graph: + +`1 (1) +/ +2 (1) +/ \ +3 (2) 4 (2) +/ \ +5 (1) 8 (1) +/ \ +6 (3) 7 (2)` Possible output (order of rows is implementation-defined) + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 2 | 1 | + +Or as a forest: + +`1 (1) 3 (2) 4 (2) 6 (3) +| | +2 (1) 7 (2) +/ \ +5 (1) 8 (1)`Returns: TableOrSubquery, The returned table has the schema (id LONG, parent_id LONG, group_key LONG). + +| Argument | Type | Description | +|---|---|---| +| tree_table | TableOrSubquery | A table/view/subquery corresponding to a tree which should be partitioned. This table must have the columns "id", "parent_id" and "group_key". Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | + +### graphs.search + +#### Macros + +**graph_reachable_dfs** + +Computes the "reachable" set of nodes in a directed graph from a given set +of starting nodes by performing a depth-first search on the graph. The +returned nodes are structured as a tree with parent-child relationships +corresponding to the order in which nodes were encountered by the DFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: + +`-- Compute the reachable nodes from the first heap root. +SELECT * +FROM graph_reachable_dfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, parent_node_id LONG). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +**graph_reachable_bfs** + +Computes the "reachable" set of nodes in a directed graph from a given +starting node by performing a breadth-first search on the graph. The returned +nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the BFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: + +`-- Compute the reachable nodes from all heap roots. +SELECT * +FROM graph_reachable_bfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, parent_node_id LONG). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +**graph_next_sibling** + +Computes the next sibling node in a directed graph. The next node under a parent node +is determined by on the \|sort_key\|, which should be unique for every node under a parent. +The order of the next sibling is undefined if the \|sort_key\| is not unique. + +Example usage: + +`-- Compute the next sibling: +SELECT * +FROM graph_next_sibling!( +( +SELECT +id AS node_id, +parent_id AS node_parent_id, +ts AS sort_key +FROM slice +) +);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, next_node_id LONG). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph for which to find the next sibling. This table must have the columns "node_id", "node_parent_id" and "sort_key". | + +**graph_reachable_weight_bounded_dfs** + +Computes the "reachable" set of nodes in a directed graph from a set of +starting (root) nodes by performing a depth-first search from each root node on the graph. +The search is bounded by the sum of edge weights on the path and the root node specifies the +max weight (inclusive) allowed before stopping the search. +The returned nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the DFS. Each row also has the root node from +which where the edge was encountered. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces with sched info: + +\`-- Compute the reachable nodes from a sched wakeup chain +INCLUDE PERFETTO MODULE sched.thread_executing_spans; + +SELECT \* +FROM +graph_reachable_dfs_bounded +!( +( +SELECT +id AS source_node_id, +COALESCE(parent_id, id) AS dest_node_id, +id - COALESCE(parent_id, id) AS edge_weight +FROM _wakeup_chain +), +( +SELECT +id AS root_node_id, +id - COALESCE(prev_id, id) AS root_target_weight +FROM _wakeup_chain +));\`Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id LONG, parent_node_id LONG). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph and an "edge_weight" corresponding to the weight of the edge between the node. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to | +| is_target_weight_floor | Expr | Whether the target_weight is a floor weight or ceiling weight. If it's floor, the search stops right after we exceed the target weight, and we include the node that pushed just passed the target. If ceiling, the search stops right before the target weight and the node that would have pushed us passed the target is not included. | + +## Package: sched + +### sched.latency + +#### Views/Tables + +**sched_latency_for_running_interval** + +TABLE +Scheduling latency of running thread states. +For each time the thread was running, returns the duration of the runnable +state directly before. + +| Column | Type | Description | +|---|---|---| +| thread_state_id | JOINID(thread_state.id) | Running state of the thread. | +| sched_id | JOINID(sched.id) | Id of a corresponding slice in a `sched` table. | +| utid | JOINID(thread.id) | Thread with running state. | +| runnable_latency_id | JOINID(thread_state.id) | Runnable state before thread is "running". Duration of this thread state is `latency_dur`. One of `thread_state.id`. | +| latency_dur | LONG | Scheduling latency of thread state. Duration of thread state with `runnable_latency_id`. | + +### sched.runnable + +#### Views/Tables + +**sched_previous_runnable_on_thread** + +TABLE +Previous runnable slice on the same thread. +For each "Running" thread state finds: + +- previous "Runnable" (or runnable preempted) state. +- previous uninterrupted "Runnable" state with a valid waker thread. + +| Column | Type | Description | +|---|---|---| +| id | JOINID(thread_state.id) | Running thread state | +| prev_runnable_id | JOINID(thread_state.id) | Previous runnable thread state. | +| prev_wakeup_runnable_id | JOINID(thread_state.id) | Previous runnable thread state with valid waker thread. | + +### sched.states + +#### Functions + +**sched_state_to_human_readable_string** + +Translates a single-letter scheduling state to a human-readable string. +Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). + +| Argument | Type | Description | +|---|---|---| +| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | + +**sched_state_io_to_human_readable_string** + +Translates a single-letter scheduling state and IO wait information to +a human-readable string. +Returns STRING: A human readable string with information about the scheduling state and IO wait. + +| Argument | Type | Description | +|---|---|---| +| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | + +### sched.thread_level_parallelism + +#### Views/Tables + +**sched_runnable_thread_count** + +TABLE +The count of runnable threads over time. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp when the runnable thread count changed to the current value. | +| runnable_thread_count | LONG | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | + +**sched_uninterruptible_sleep_thread_count** + +TABLE +The count of threads in uninterruptible sleep over time. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp when the thread count changed to the current value. | +| uninterruptible_sleep_thread_count | LONG | Number of threads in uninterrutible sleep, covering the range from this timestamp to the next row's timestamp. | + +**sched_active_cpu_count** + +TABLE +The count of active CPUs over time. + +| Column | Type | Description | +|---|---|---| +| ts | TIMESTAMP | Timestamp when the number of active CPU changed. | +| active_cpu_count | LONG | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | + +### sched.time_in_state + +#### Views/Tables + +**sched_time_in_state_for_thread** + +TABLE +The time a thread spent in each scheduling state during it's lifetime. + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Utid of the thread. | +| total_runtime | LONG | Total runtime of thread. | +| state | STRING | One of the scheduling states of kernel thread. | +| time_in_state | LONG | Total time spent in the scheduling state. | +| percentage_in_state | LONG | Percentage of time thread spent in scheduling state in \[0-100\] range. | + +**sched_percentage_of_time_in_state** + +TABLE +Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] +ranges). Sum of all states might be smaller than 100, as those values +are rounded down. + +| Column | Type | Description | +|---|---|---| +| utid | JOINID(thread.id) | Utid of the thread. | +| running | LONG | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | +| runnable | LONG | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | +| runnable_preempted | LONG | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | +| sleeping | LONG | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | +| uninterruptible_sleep | LONG | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | +| other | LONG | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | + +#### Table Functions + +**sched_time_in_state_for_thread_in_interval** + +Time the thread spent each state in a given interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +Specifically for any non-trivial subset of thread slices, prefer using +`thread_slice_time_in_state` in the `slices.time_in_state` module for this +purpose instead. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | The start of the interval. | +| dur | DURATION | The duration of the interval. | +| utid | JOINID(thread.id) | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | STRING | The scheduling state (from the `thread_state` table). Use the `sched_state_to_human_readable_string` function in the `sched` package to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| blocked_function | LONG | If the `state` is uninterruptible sleep, `io_wait` indicates if it was an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if we cannot tell if it was an IO sleep or not. Only available on Android when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | +| dur | DURATION | The duration of time the threads slice spent for each (state, io_wait, blocked_function) tuple. | + +**sched_time_in_state_and_cpu_for_thread_in_interval** + +Time the thread spent each state and cpu in a given interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| ts | TIMESTAMP | The start of the interval. | +| dur | DURATION | The duration of the interval. | +| utid | JOINID(thread.id) | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | STRING | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| cpu | LONG | Id of the CPU. | +| blocked_function | LONG | Some states can specify the blocked function. Usually NULL. | +| dur | DURATION | Total time spent with this state, cpu and blocked function. | + +**sched_time_in_state_for_cpu_in_interval** + +Time spent by CPU in each scheduling state in a provided interval. + +This function is only designed to run over a small number of intervals +(10-100 at most). It will be *very slow* for large sets of intervals. + +| Argument | Type | Description | +|---|---|---| +| cpu | LONG | CPU id. | +| ts | TIMESTAMP | Interval start. | +| dur | LONG | Interval duration. | + +| Column | Type | Description | +|---|---|---| +| end_state | STRING | End state. From `sched.end_state`. | +| dur | LONG | Duration in state. | + +### sched.with_context + +#### Views/Tables + +**sched_with_thread_process** + +VIEW +View of scheduling slices with extended information. +It holds slices with kernel thread scheduling information. These slices are +collected when the Linux "ftrace" data source is used with the +"sched/switch" and "sched/wakeup\*" events enabled. + +The rows in this table will always have a matching row in the \|thread_state\| +table with \|thread_state.state\| = 'Running' + +| Column | Type | Description | +|---|---|---| +| id | ID(sched.id) | Unique identifier for this scheduling slice (Running period). | +| ts | TIMESTAMP | The timestamp at the start of the Running period. | +| dur | DURATION | The duration of the Running period. | +| utid | JOINID(thread.id) | Unique identifier of the thread that was running. | +| thread_name | STRING | Name of the thread that was running. | +| upid | JOINID(process.id) | Unique identifier of the process that the thread belongs to. | +| process_name | STRING | Name of the process that the thread belongs to. | +| cpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | LONG | The kernel priority that the thread ran at. | + +## Package: export + +### export.to_firefox_profile + +#### Functions + +**export_to_firefox_profile** + +Dumps all trace data as a Firefox profile json string +See `Profile` in + +Also + + +You would probably want to download the generated json and then open at + +You can easily do this from the UI via the following SQL +`SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` +The result will have a link for you to download this json as a file. +Returns STRING: Json profile + +## Package: traced + +### traced.stats + +#### Views/Tables + +**traced_clone_flush_latency** + +TABLE +Reports the duration of the flush operation for cloned traces (for each +buffer). + +| Column | Type | Description | +|---|---|---| +| buffer_id | LONG | Id of the buffer (matches the config). | +| duration_ns | LONG | Interval from the start of the clone operation to the end of the flush for this buffer. | + +**traced_trigger_clone_flush_latency** + +TABLE +Reports the delay in finalizing the trace from the trigger that causes the +clone operation. + +| Column | Type | Description | +|---|---|---| +| buffer_id | LONG | Id of the buffer. | +| duration_ns | LONG | Interval from the trigger that caused the clone operation to the end of the flush for this buffer. | + +## Package: pkvm + +### pkvm.hypervisor + +#### Views/Tables + +**pkvm_hypervisor_events** + +VIEW +Events when CPU entered hypervisor. + +| Column | Type | Description | +|---|---|---| +| slice_id | JOINID(slice.id) | Id of the corresponding slice in slice table. | +| cpu | JOINID(cpu.cpu) | CPU that entered hypervisor. | +| ts | TIMESTAMP | Timestamp when CPU entered hypervisor. | +| dur | DURATION | How much time CPU spent in hypervisor. | +| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | + +## Package: stacks + +### stacks.cpu_profiling + +#### Views/Tables + +**cpu_profiling_samples** + +TABLE +Table containing all the timestamped samples of CPU profiling which occurred +during the trace. + +Currently, this table is backed by the following data sources: + +- Linux perf +- Simpleperf proto format +- macOS instruments +- Chrome CPU profiling +- Legacy V8 CPU profiling +- Profiling data in Gecko traces + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the sample. | +| ts | TIMESTAMP | The timestamp of the sample. | +| utid | JOINID(thread.id) | The utid of the thread of the sample, if available. | +| tid | LONG | The tid of the sample, if available. | +| thread_name | STRING | The thread name of thread of the sample, if available. | +| ucpu | LONG | The ucpu of the sample, if available. | +| cpu | LONG | The cpu of the sample, if available. | +| callsite_id | LONG | The callsite id of the sample. | + +**cpu_profiling_summary_tree** + +TABLE +Table summarising the callstacks captured during any CPU profiling which +occurred during the trace. + +Specifically, this table returns a tree containing all the callstacks seen +during the trace with `self_count` equal to the number of samples with that +frame as the leaf and `cumulative_count` equal to the number of samples with +the frame anywhere in the tree. + +The data sources supported are the same as the `cpu_profiling_samples` table. + +| Column | Type | Description | +|---|---|---| +| id | LONG | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | +| parent_id | LONG | The id of the parent callstack for this callstack. NULL if this is root. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | LONG | The line number in the file the function is located at. | +| self_count | LONG | The number of samples with this function as the leaf frame. | +| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | + +## Package: time + +### time.conversion + +#### Functions + +**time_from_ns** + +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns TIMESTAMP: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | LONG | Time duration in nanoseconds. | + +**time_from_us** + +Converts a duration in microseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns LONG: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| micros | LONG | Time duration in microseconds. | + +**time_from_ms** + +Converts a duration in millseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns TIMESTAMP: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| millis | LONG | Time duration in milliseconds. | + +**time_from_s** + +Converts a duration in seconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns TIMESTAMP: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| seconds | LONG | Time duration in seconds. | + +**time_from_min** + +Converts a duration in minutes to nanoseconds, which is the default +representation of time durations in trace processor. +Returns TIMESTAMP: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| minutes | LONG | Time duration in minutes. | + +**time_from_hours** + +Converts a duration in hours to nanoseconds, which is the default +representation of time durations in trace processor. +Returns TIMESTAMP: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| hours | LONG | Time duration in hours. | + +**time_from_days** + +Converts a duration in days to nanoseconds, which is the default +representation of time durations in trace processor. +Returns LONG: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| days | LONG | Time duration in days. | + +**time_to_ns** + +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns LONG: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_us** + +Converts a duration in nanoseconds to microseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in microseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_ms** + +Converts a duration in nanoseconds to millseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in milliseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_s** + +Converts a duration in nanoseconds to seconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in seconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_min** + +Converts a duration in nanoseconds to minutes. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in minutes. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_hours** + +Converts a duration in nanoseconds to hours. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in hours. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +**time_to_days** + +Converts a duration in nanoseconds to days. Nanoseconds is the default +representation of time durations in trace processor. +Returns LONG: Time duration in days. + +| Argument | Type | Description | +|---|---|---| +| nanos | TIMESTAMP | Time duration in nanoseconds. | + +Except as otherwise noted, the content of this page is licensed under the +[Creative Commons +Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), and code samples are licensed +under the [Apache 2.0 +License](https://www.apache.org/licenses/LICENSE-2.0). Java is a registered trademark of Oracle and/or its affiliates. + +- [Site CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) +- [Privacy](https://www.google.com/intl/en/policies/privacy/) \ No newline at end of file diff --git a/system/edge-to-edge/SKILL.md b/system/edge-to-edge/SKILL.md index a4a4207..f618ab2 100644 --- a/system/edge-to-edge/SKILL.md +++ b/system/edge-to-edge/SKILL.md @@ -7,6 +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-04-01' keywords: - android - compose @@ -422,4 +423,4 @@ Dialog( - \[ \] Does every `TextField`, `OutlinedTextField`, or `BasicTextField` have a parent with `imePadding()`, `fitInside`, `Modifier.safeDrawingPadding()`, `Modifier.safeContentPadding()`, `Modifier.safeGesturesPadding()`, or `contentWindowInsets` set to `WindowInsets.safeDrawing` or `WindowInsets.ime`? - \[\] Does the first and last list item draw away from the system bars by passing insets to `contentPadding`? - \[\] Do FABs draw above the navigation bars by either being inside a Scaffold or by applying `Modifier.safeDrawingPadding()`? -- \[\] Does the project build? Run `./gradlew build` to be sure. \ No newline at end of file +- \[\] Does the project build? Run `./gradlew build` to be sure. diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md index 85c8dea..ee09ddf 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,6 +9,7 @@ description: Provides guidelines for developing projected applications for Andro license: Complete terms in LICENSE.txt metadata: author: Google LLC + last-updated: '2026-05-08' keywords: - Jetpack Compose Glimmer - AI Glasses @@ -319,4 +320,4 @@ component. - For a detailed breakdown of notifications on AI Glasses, see [Understand notification behavior for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md) and learn how to [Start a glasses - activity on display AI glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). \ No newline at end of file + activity on display AI glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md index 6e8767d..12fec60 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md @@ -28,7 +28,7 @@ Add the following [library dependencies for AI glasses](https://developer.androi ### Groovy dependencies { - implementation "androidx.xr.runtime:runtime:1.0.0-alpha12" + implementation "androidx.xr.runtime:runtime:1.0.0-alpha13" implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha11" implementation "androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11" implementation "androidx.xr.projected:projected:1.0.0-alpha06" @@ -38,7 +38,7 @@ Add the following [library dependencies for AI glasses](https://developer.androi ### Kotlin dependencies { - implementation("androidx.xr.runtime:runtime:1.0.0-alpha12") + implementation("androidx.xr.runtime:runtime:1.0.0-alpha13") implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha11") implementation("androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11") implementation("androidx.xr.projected:projected:1.0.0-alpha06") diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md index 902f60f..fba367d 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md @@ -33,11 +33,11 @@ import androidx.xr.glimmer.ButtonSize import androidx.xr.glimmer.GlimmerTheme import androidx.xr.glimmer.Icon import androidx.xr.glimmer.Text -import androidx.xr.glimmer.list.VerticalList +import androidx.xr.glimmer.list.GlimmerLazyColumn @Composable fun ButtonSampleUsage() { - VerticalList( + GlimmerLazyColumn( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), ) { diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md index 0d89012..304e3b2 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md @@ -72,9 +72,9 @@ import androidx.compose.ui.unit.dp * the provided [buttonSize] will affect other properties such as padding values and the size of * icons. * @param leadingIcon optional leading icon to be placed before the [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after the [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this button, and also used to draw the background and * border * @param color background color of this button diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md index ce9da11..1586776 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md @@ -38,11 +38,11 @@ import androidx.xr.glimmer.Card import androidx.xr.glimmer.GlimmerTheme import androidx.xr.glimmer.Icon import androidx.xr.glimmer.Text -import androidx.xr.glimmer.list.VerticalList +import androidx.xr.glimmer.list.GlimmerLazyColumn @Composable fun CardSampleUsage() { - VerticalList { + GlimmerLazyColumn { item { CardSample() } item { CardWithTrailingIconSample() } item { CardWithTitleAndSubtitleAndLeadingIconSample() } diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md index 7eb8d9b..c260bfe 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md @@ -88,9 +88,9 @@ import kotlin.math.max * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum * aspect ratio. * @param leadingIcon optional leading icon to be placed before [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this card, and also used to draw the background and border * @param color background color of this card * @param contentColor content color used by components inside [content], [title], [subtitle], @@ -184,9 +184,9 @@ public fun Card( * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum * aspect ratio. * @param leadingIcon optional leading icon to be placed before [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this card, and also used to draw the background and border * @param color background color of this card * @param contentColor content color used by components inside [content], [title], [subtitle], @@ -270,9 +270,9 @@ public fun Card( * [androidx.compose.foundation.layout.fillMaxSize] will result in an image that fills the maximum * aspect ratio. * @param leadingIcon optional leading icon to be placed before [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this card, and also used to draw the background and border * @param color background color of this card * @param contentColor content color used by components inside [content], [title], [subtitle], diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md index d414811..59f9e71 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md @@ -44,8 +44,8 @@ import androidx.compose.ui.unit.dp /** * ListItem is a component used to represent a single item in a - * [androidx.xr.glimmer.list.VerticalList]. A ListItem has a primary label [content], and may also - * have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may + * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting * label is displayed below the primary label and can be used to provide additional information. A * ListItem fills the maximum width available by default. * @@ -66,9 +66,9 @@ import androidx.compose.ui.unit.dp * @param supportingLabel optional supporting label to be placed underneath the primary label * [content] * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is - * typically an [Icon]. + * typically an [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This - * is typically an [Icon]. + * is typically an [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this list item, and also used to draw the background and * border * @param color background color of this list item @@ -115,8 +115,8 @@ public fun ListItem( /** * ListItem is a component used to represent a single item in a - * [androidx.xr.glimmer.list.VerticalList]. A ListItem has a primary label [content], and may also - * have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may + * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting * label is displayed below the primary label and can be used to provide additional information. A * ListItem fills the maximum width available by default. * @@ -139,9 +139,9 @@ public fun ListItem( * @param supportingLabel optional supporting label to be placed underneath the primary label * [content] * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is - * typically an [Icon]. + * typically an [Icon] tinted with [contentColor] by default. * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This - * is typically an [Icon]. + * is typically an [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this list item, and also used to draw the background and * border * @param color background color of this list item diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md index 922a179..925a5cd 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md @@ -51,6 +51,7 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.lerp import androidx.compose.ui.zIndex +import androidx.xr.glimmer.internal.SingleItemScrollConstraintConnection import kotlin.math.roundToInt /** diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md index 2f44dba..03101d1 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md @@ -37,11 +37,11 @@ import androidx.xr.glimmer.Icon import androidx.xr.glimmer.Text import androidx.xr.glimmer.TitleChip import androidx.xr.glimmer.TitleChipDefaults -import androidx.xr.glimmer.list.VerticalList +import androidx.xr.glimmer.list.GlimmerLazyColumn @Composable fun TitleChipSampleUsage() { - VerticalList( + GlimmerLazyColumn( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), ) { diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md index a876a81..8b98176 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md +++ b/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md @@ -58,7 +58,7 @@ import androidx.compose.ui.unit.dp * @sample androidx.xr.glimmer.samples.TitleChipWithCardSample * @param modifier the [Modifier] to be applied to this title chip * @param leadingIcon optional leading icon to be placed before the [content]. This is typically an - * [Icon]. + * [Icon] tinted with [contentColor] by default. * @param shape the [Shape] used to clip this title chip, and also used to draw the background and * border * @param color background color of this title chip From 0772dbfa57d40e1d4796f873e9cad521cd0cdbf7 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Wed, 13 May 2026 18:07:01 +0000 Subject: [PATCH 15/61] Updates skills (2026-05-13 18:07) --- profilers/perfetto-sql/SKILL.md | 2 +- .../references/perfetto-stdlib-docs.md | 11969 ++++++++-------- profilers/perfetto-trace-analysis/SKILL.md | 81 + .../skills/profilers/perfetto-sql/skill.md | 126 + .../SKILL.md | 88 +- .../ai-glasses/guides/interaction/inputs.md | 0 .../access-hardware-projected-context.md | 0 .../ai-glasses/check-capabilities.md | 0 .../ai-glasses/first-activity.md | 16 +- .../ai-glasses/notifications/behavior.md | 0 .../notifications/start-activity.md | 0 .../jetpack-compose-glimmer/buttons.md | 0 .../jetpack-compose-glimmer/cards.md | 0 .../jetpack-compose-glimmer/focus.md | 0 .../jetpack-compose-glimmer/icons.md | 0 .../jetpack-compose-glimmer/text.md | 0 .../jetpack-compose-glimmer/title-chips.md | 0 .../request-hardware-permissions.md | 0 .../references/button-samples-source.md | 0 .../references/button-source.md | 1 + .../references/card-samples-source.md | 0 .../references/card-source.md | 0 .../references/deptheffect-source.md | 0 .../references/deptheffectlevels-source.md | 0 .../glimmersansflextypography-source.md | 0 .../references/glimmertheme-source.md | 0 .../references/icon-source.md | 0 .../references/iconbutton-source.md | 1 + .../references/iconsizes-source.md | 0 .../references/list-source.md | 0 .../references/listitem-source.md | 0 .../references/liststate-source.md | 0 .../references/material-hct-source.md | 0 .../references/projectedcontext-source.md | 0 .../references/stack-source.md | 0 .../references/stackitemscope-source.md | 0 .../references/stackstate-source.md | 0 .../references/surface-source.md | 54 +- .../references/titlechip-samples-source.md | 0 .../references/titlechip-source.md | 0 40 files changed, 6115 insertions(+), 6223 deletions(-) create mode 100644 profilers/perfetto-trace-analysis/SKILL.md create mode 100644 profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/SKILL.md (75%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/design/ui/ai-glasses/guides/interaction/inputs.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md (98%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/button-samples-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/button-source.md (99%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/card-samples-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/card-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/deptheffect-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/deptheffectlevels-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/glimmersansflextypography-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/glimmertheme-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/icon-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/iconbutton-source.md (99%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/iconsizes-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/list-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/listitem-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/liststate-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/material-hct-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/projectedcontext-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/stack-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/stackitemscope-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/stackstate-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/surface-source.md (94%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/titlechip-samples-source.md (100%) rename xr/{display-ai-glasses-with-jetpack-compose-glimmer => display-glasses-with-jetpack-compose-glimmer}/references/titlechip-source.md (100%) diff --git a/profilers/perfetto-sql/SKILL.md b/profilers/perfetto-sql/SKILL.md index 6bbd0d9..3d0cff4 100644 --- a/profilers/perfetto-sql/SKILL.md +++ b/profilers/perfetto-sql/SKILL.md @@ -6,7 +6,7 @@ description: Translates natural language data intents into syntactically valid P license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-07' + last-updated: '2026-05-13' keywords: - Android - Perfetto SQL diff --git a/profilers/perfetto-sql/references/perfetto-stdlib-docs.md b/profilers/perfetto-sql/references/perfetto-stdlib-docs.md index ed4f4e9..1740395 100644 --- a/profilers/perfetto-sql/references/perfetto-stdlib-docs.md +++ b/profilers/perfetto-sql/references/perfetto-stdlib-docs.md @@ -3,681 +3,111 @@ ## Introduction The PerfettoSQL standard library is a repository of tables, views, functions -and macros, contributed by domain experts, which make querying traces easier. +and macros, contributed by domain experts, which make querying traces easier Its design is heavily inspired by standard libraries in languages like Python, C++ and Java. Some of the purposes of the standard library include: - -1. Acting as a way of sharing and commonly written queries without needing to copy/paste large amounts of SQL. -2. Raising the abstraction level when exposing data in the trace. Many modules in the standard library convert low-level trace concepts e.g. slices, tracks and into concepts developers may be more familar with e.g. for Android developers: app startups, binder transactions etc. +1) Acting as a way of sharing and commonly written queries without needing +to copy/paste large amounts of SQL. +2) Raising the abstraction level when exposing data in the trace. Many +modules in the standard library convert low-level trace concepts +e.g. slices, tracks and into concepts developers may be more familar with +e.g. for Android developers: app startups, binder transactions etc. Standard library modules can be included as follows: - -\`-- Include all tables/views/functions from the android.startup.startups +\`\`\` +-- Include all tables/views/functions from the android.startup.startups -- module in the standard library. INCLUDE PERFETTO MODULE android.startup.startups; -- Use the android_startups table defined in the android.startup.startups -- module. SELECT \* -FROM android_startups;\`Prelude is a special module is automatically included. It contains key helper +FROM android_startups; +\`\`\` + +Prelude is a special module is automatically included. It contains key helper tables, views and functions which are universally useful. More information on importing modules is available in the -[syntax documentation](https://perfetto.dev/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) +[syntax documentation](https://developer.android.com/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) for the `INCLUDE PERFETTO MODULE` statement. ## Package: prelude -### prelude - #### Views/Tables -**perf_sample** +
-VIEW -Samples from the traced_perf profiler. +**trace_metrics**. Lists all metrics built-into trace processor. + +
+ +TABLE | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this perf sample. | -| ts | TIMESTAMP | Timestamp of the sample. | -| utid | JOINID(thread.id) | Sampled thread. | -| cpu | LONG | Core the sampled thread was running on. | -| cpu_mode | STRING | Execution state (userspace/kernelspace) of the sampled thread. | -| callsite_id | JOINID(stack_profile_callsite.id) | If set, unwound callstack of the sampled thread. | -| unwind_error | STRING | If set, indicates that the unwinding for this sample encountered an error. Such samples still reference the best-effort result via the callsite_id, with a synthetic error frame at the point where unwinding stopped. | -| perf_session_id | JOINID(perf_session.id) | Distinguishes samples from different profiling streams (i.e. multiple data sources). | +| name | STRING | The name of the metric. | -**counter** +
-VIEW -Counters are values put into tracks during parsing of the trace. +
-| Column | Type | Description | -|---|---|---| -| id | ID | Unique id of a counter value | -| ts | TIMESTAMP | Time of fetching the counter value. | -| track_id | JOINID(track.id) | Track this counter value belongs to. | -| value | DOUBLE | Value. | -| arg_set_id | ARGSETID | Additional information about the counter value. | +
-**slice** +**track**. Tracks are a fundamental concept in trace processor and represent a "timeline" for events of the same type and with the same context -VIEW -Contains slices from userspace which explains what threads were doing -during the trace. - -| Column | Type | Description | -|---|---|---| -| id | ID | The id of the slice. | -| ts | TIMESTAMP | The timestamp at the start of the slice in nanoseconds. The actual value depends on the `primary_trace_clock` selected in TraceConfig. This is often the value of a monotonic counter since device boot so is only meaningful in the context of a trace. | -| dur | DURATION | The duration of the slice in nanoseconds. | -| track_id | JOINID(track.id) | The id of the track this slice is located on. | -| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | -| name | STRING | The name of the slice. The name describes what was happening during the slice. | -| depth | LONG | The depth of the slice in the current stack of slices. | -| parent_id | JOINID(slice.id) | The id of the parent (i.e. immediate ancestor) slice for this slice. | -| arg_set_id | ARGSETID | The id of the argument set associated with this slice. | -| thread_ts | TIMESTAMP | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_dur | DURATION | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| cat | STRING | Alias of `category`. | -| slice_id | JOINID(slice.id) | Alias of `id`. | - -**instant** - -VIEW -Contains instant events from userspace which indicates what happened at a -single moment in time. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | The timestamp of the instant. | -| track_id | JOINID(track.id) | The id of the track this instant is located on. | -| name | STRING | The name of the instant. The name describes what happened during the instant. | -| arg_set_id | ARGSETID | The id of the argument set associated with this instant. | - -**slices** - -VIEW -Alternative alias of table `slice`. - -| Column | Type | Description | -|---|---|---| -| id | JOINID(slice.id) | Alias of `slice.id`. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | -| thread_dur | DURATION | Alias of `slice.thread_dur`. | -| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | -| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | -| cat | STRING | Alias of `slice.cat`. | -| slice_id | JOINID(slice.id) | Alias of `slice.slice_id`. | - -**thread** - -VIEW -Contains information of threads seen during the trace. - -| Column | Type | Description | -|---|---|---| -| id | ID | The id of the thread. Prefer using `utid` instead. | -| utid | ID | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | -| tid | LONG | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use | -| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | -| start_ts | TIMESTAMP | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | -| end_ts | TIMESTAMP | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | -| upid | JOINID(process.id) | The process hosting this thread. | -| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | -| is_idle | BOOL | Boolean indicating if this thread is a kernel idle thread. | -| machine_id | LONG | Machine identifier, non-null for threads on a remote machine. | -| arg_set_id | ARGSETID | Extra args for this thread. | - -**process** - -VIEW -Contains information of processes seen during the trace. - -| Column | Type | Description | -|---|---|---| -| id | ID | The id of the process. Prefer using `upid` instead. | -| upid | JOINID(process.id) | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | -| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use | -| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | -| start_ts | TIMESTAMP | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | -| end_ts | TIMESTAMP | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | -| parent_upid | JOINID(process.id) | The upid of the process which caused this process to be spawned. | -| uid | LONG | The Unix user id of the process. | -| android_appid | LONG | Android appid of this process. | -| android_user_id | LONG | Android user id of this process. | -| cmdline | STRING | /proc/cmdline for this process. | -| arg_set_id | ARGSETID | Extra args for this process. | -| machine_id | LONG | Machine identifier, non-null for processes on a remote machine. | - -**args** - -VIEW -Arbitrary key-value pairs which allow adding metadata to other, strongly -typed tables. -Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| -will be non-null. - -| Column | Type | Description | -|---|---|---| -| id | ID | The id of the arg. | -| arg_set_id | ARGSETID | The id for a single set of arguments. | -| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | -| key | STRING | The key for the arg. | -| int_value | LONG | The integer value of the arg. | -| string_value | STRING | The string value of the arg. | -| real_value | DOUBLE | The double value of the arg. | -| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | -| display_value | STRING | The human-readable formatted value of the arg. | - -**perf_session** - -VIEW -Contains the Linux perf sessions in the trace. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The id of the perf session. Prefer using `perf_session_id` instead. | -| perf_session_id | LONG | The id of the perf session. | -| cmdline | STRING | Command line used to collect the data. | - -**android_logs** - -VIEW -Log entries from Android logcat. - -NOTE: this table is not sorted by timestamp. - -| Column | Type | Description | -|---|---|---| -| id | ID | Which row in the table the log corresponds to. | -| ts | TIMESTAMP | Timestamp of log entry. | -| utid | JOINID(thread.id) | Thread writing the log entry. | -| prio | LONG | Priority of the log. 3=DEBUG, 4=INFO, 5=WARN, 6=ERROR. | -| tag | STRING | Tag of the log entry. | -| msg | STRING | Content of the log entry | - -**track** +
VIEW Tracks are a fundamental concept in trace processor and represent a "timeline" for events of the same type and with the same context. See - for a more +https://perfetto.dev/docs/analysis/trace-processor#tracks for a more detailed explanation, with examples. | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this track. Identical to | +| id | UINT | Unique identifier for this track. Identical to \|track_id\|, prefer using \|track_id\| instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | | name | STRING | Name of the track; can be null for some types of tracks (e.g. thread tracks). | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given `type`. Join with the `args` table or use the `EXTRACT_ARG` helper function to expand the args. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Generic key-value pairs containing extra information about the track. Join with the `args` table or use the `EXTRACT_ARG` helper function to expand the args. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| track_group_id | LONG | An opaque key indicating that this track belongs to a group of tracks which are "conceptually" the same track. Tracks in trace processor don't allow overlapping events to allow for easy analysis (i.e. SQL window functions, SPAN JOIN and other similar operators). However, in visualization settings (e.g. the UI), the distinction doesn't matter and all tracks with the same `track_group_id` should be merged together into a single logical "UI track". | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. Alias of `args.arg_set_id`. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | -**thread_track** +
-TABLE -Tracks which are associated to a single thread. +
-| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this thread track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| utid | JOINID(thread.id) | The utid that the track is associated with. | +
-**process_track** +**cpu**. Contains information about the CPUs on the device this trace was taken on. -TABLE -Tracks which are associated to a single process. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this process track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| upid | JOINID(process.id) | The upid that the track is associated with. | - -**cpu_track** - -TABLE -Tracks which are associated to a single CPU. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this cpu track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| cpu | LONG | The CPU that the track is associated with. | - -**gpu_track** - -TABLE -Table containing tracks which are loosely tied to a GPU. - -NOTE: this table is deprecated due to inconsistency of it's design with -other track tables (e.g. not having a GPU column, mixing a bunch of different -tracks which are barely related). Please use the track table directly -instead. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this cpu track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given type. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| scope | STRING | The source of the track. Deprecated. | -| description | STRING | The description for the track. | -| context_id | LONG | The context id for the GPU this track is associated to. | - -**ftrace_event** - -VIEW -Contains all the ftrace events in the trace. This table exists only for -debugging purposes and should not be relied on in production usecases (i.e. -metrics, standard library etc). Note also that this table might be empty if -raw ftrace parsing has been disabled. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this ftrace event. | -| ts | TIMESTAMP | The timestamp of this event. | -| name | STRING | The ftrace event name. | -| cpu | LONG | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | JOINID(thread.id) | The thread this event was emitted on. | -| arg_set_id | ARGSETID | The set of key/value pairs associated with this event. | -| common_flags | LONG | Ftrace event flags for this event. Currently only emitted for sched_waking events. | -| ucpu | LONG | The unique CPU identifier that this event was emitted on. | - -**raw** - -VIEW -This table is deprecated. Use `ftrace_event` instead which contains the same -rows; this table is simply a (badly named) alias. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this raw event. | -| ts | TIMESTAMP | The timestamp of this event. | -| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | -| cpu | LONG | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | JOINID(thread.id) | The thread this event was emitted on. | -| arg_set_id | ARGSETID | The set of key/value pairs associated with this event. | -| common_flags | LONG | Ftrace event flags for this event. Currently only emitted for sched_waking events. | -| ucpu | LONG | The unique CPU identifier that this event was emitted on. | - -**frame_slice** - -VIEW -Table containing graphics frame events on Android. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Alias of `slice.id`. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | LONG | Alias of `slice.arg_set_id`. | -| layer_name | STRING | Name of the graphics layer this slice happened on. | -| frame_number | LONG | The frame number this slice is associated with. | -| queue_to_acquire_time | LONG | The time between queue and acquire for this buffer and layer. | -| acquire_to_latch_time | LONG | The time between acquire and latch for this buffer and layer. | -| latch_to_present_time | LONG | The time between latch and present for this buffer and layer. | - -**gpu_slice** - -VIEW -Table containing graphics frame events on Android. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Alias of `slice.id`. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | LONG | Alias of `slice.arg_set_id`. | -| context_id | LONG | Context ID. | -| render_target | LONG | Render target ID. | -| render_target_name | STRING | The name of the render target. | -| render_pass | LONG | Render pass ID. | -| render_pass_name | STRING | The name of the render pass. | -| command_buffer | LONG | The command buffer ID. | -| command_buffer_name | STRING | The name of the command buffer. | -| frame_id | LONG | Frame id. | -| submission_id | LONG | The submission id. | -| hw_queue_id | LONG | The hardware queue id. | -| upid | JOINID(process.id) | The id of the process. | -| render_subpasses | STRING | Render subpasses. | - -**expected_frame_timeline_slice** - -TABLE -This table contains information on the expected timeline of either a display -frame or a surface frame. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Alias of `slice.id`. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | LONG | Alias of `slice.arg_set_id`. | -| display_frame_token | LONG | Display frame token (vsync id). | -| surface_frame_token | LONG | Surface frame token (vsync id), null if this is a display frame. | -| upid | JOINID(process.id) | Unique process id of the app that generates the surface frame. | -| layer_name | STRING | Layer name if this is a surface frame. | - -**actual_frame_timeline_slice** - -TABLE -This table contains information on the actual timeline and additional -analysis related to the performance of either a display frame or a surface -frame. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Alias of `slice.id`. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(frame_slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | LONG | Alias of `slice.arg_set_id`. | -| display_frame_token | LONG | Display frame token (vsync id). | -| upid | JOINID(process.id) | Surface frame token (vsync id), null if this is a display frame. | -| surface_frame_token | LONG | Unique process id of the app that generates the surface frame. | -| layer_name | STRING | Layer name if this is a surface frame. | -| present_type | STRING | Frame's present type (eg. on time / early / late). | -| on_time_finish | LONG | Whether the frame finishes on time. | -| gpu_composition | LONG | Whether the frame used gpu composition. | -| jank_type | STRING | Specify the jank types for this frame if there's jank, or none if no jank occurred. | -| jank_severity_type | STRING | Severity of the jank: none if no jank. | -| prediction_type | STRING | Frame's prediction type (eg. valid / expired). | -| jank_tag | STRING | Jank tag based on jank type, used for slice visualization. | -| jank_tag_experimental | STRING | Jank tag (experimental) based on jank type, used for slice visualization. | - -**heap_graph_class** - -VIEW -Stores class information within ART heap graphs. It represents Java/Kotlin -classes that exist in the heap, including their names, inheritance -relationships, and loading context. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this heap graph class. | -| name | STRING | (potentially obfuscated) name of the class. | -| deobfuscated_name | STRING | If class name was obfuscated and deobfuscation map for it provided, the deobfuscated name. | -| location | STRING | the APK / Dex / JAR file the class is contained in. | -| superclass_id | JOINID(heap_graph_class.id) | The superclass of this class. | -| classloader_id | LONG | The classloader that loaded this class. | -| kind | STRING | The kind of class. | - -**heap_graph_object** - -VIEW -The objects on the Dalvik heap. - -All rows with the same (upid, graph_sample_ts) are one dump. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this heap graph object. | -| upid | JOINID(process.id) | Unique PID of the target. | -| graph_sample_ts | TIMESTAMP | Timestamp this dump was taken at. | -| self_size | LONG | Size this object uses on the Java Heap. | -| native_size | LONG | Approximate amount of native memory used by this object, as reported by libcore.util.NativeAllocationRegistry.size. | -| reference_set_id | JOINID(heap_graph_reference.reference_set_id) | Join key with heap_graph_reference containing all objects referred in this object's fields. | -| reachable | BOOL | Bool whether this object is reachable from a GC root. If false, this object is uncollected garbage. | -| heap_type | STRING | The type of ART heap this object is stored on (app, zygote, boot image) | -| type_id | JOINID(heap_graph_class.id) | Class this object is an instance of. | -| root_type | STRING | If not NULL, this object is a GC root. | -| root_distance | LONG | Distance from the root object. | - -**heap_graph_reference** - -VIEW -Many-to-many mapping between heap_graph_object. - -This associates the object with given reference_set_id with the objects -that are referred to by its fields. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this heap graph reference. | -| reference_set_id | JOINID(heap_graph_object.reference_set_id) | Join key to heap_graph_object reference_set_id. | -| owner_id | JOINID(heap_graph_object.id) | Id of object that has this reference_set_id. | -| owned_id | JOINID(heap_graph_object.id) | Id of object that is referred to. | -| field_name | STRING | The field that refers to the object. E.g. Foo.name. | -| field_type_name | STRING | The static type of the field. E.g. java.lang.String. | -| deobfuscated_field_name | STRING | The deobfuscated name, if field_name was obfuscated and a deobfuscation mapping was provided for it. | - -**memory_snapshot** - -VIEW -Table with memory snapshots. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this snapshot. | -| timestamp | TIMESTAMP | Time of the snapshot. | -| track_id | JOINID(track.id) | Track of this snapshot. | -| detail_level | STRING | Detail level of this snapshot. | - -**process_memory_snapshot** - -VIEW -Table with process memory snapshots. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this snapshot. | -| snapshot_id | JOINID(memory_snapshot.id) | Snapshot ID for this snapshot. | -| upid | JOINID(process.id) | Process for this snapshot. | - -**memory_snapshot_node** - -VIEW -Table with memory snapshot nodes. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this node. | -| process_snapshot_id | JOINID(process_memory_snapshot.id) | Process snapshot ID for to this node. | -| parent_node_id | JOINID(memory_snapshot_node.id) | Parent node for this node, optional. | -| path | STRING | Path for this node. | -| size | LONG | Size of the memory allocated to this node. | -| effective_size | LONG | Effective size used by this node. | -| arg_set_id | ARGSETID | Additional args of the node. | - -**memory_snapshot_edge** +
VIEW | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this edge. | -| source_node_id | JOINID(memory_snapshot_node.id) | Source node for this edge. | -| target_node_id | JOINID(memory_snapshot_node.id) | Target node for this edge. | -| importance | LONG | Importance for this edge. | - -**counter_track** - -VIEW -Tracks containing counter-like events. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this cpu counter track. | -| name | STRING | Name of the track. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| dimension_arg_set_id | ARGSETID | The dimensions of the track which uniquely identify the track within a given type. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | - -**cpu_counter_track** - -TABLE -Tracks containing counter-like events associated to a CPU. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this cpu counter track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| cpu | LONG | The CPU that the track is associated with. | - -**gpu_counter_track** - -TABLE -Tracks containing counter-like events associated to a GPU. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this gpu counter track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| gpu_id | LONG | The GPU that the track is associated with. | - -**process_counter_track** - -TABLE -Tracks containing counter-like events associated to a process. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this process counter track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| upid | LONG | The upid of the process that the track is associated with. | - -**thread_counter_track** - -TABLE -Tracks containing counter-like events associated to a thread. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this thread counter track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | JOINID(track.id) | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| utid | LONG | The utid of the thread that the track is associated with. | - -**perf_counter_track** - -TABLE -Tracks containing counter-like events collected from Linux perf. - -| Column | Type | Description | -|---|---|---| -| id | ID(track.id) | Unique identifier for this thread counter track. | -| name | STRING | Name of the track. | -| type | STRING | The type of a track indicates the type of data the track contains. Every track is uniquely identified by the the combination of the type and a set of dimensions: type allow identifying a set of tracks with the same type of data within the whole universe of tracks while dimensions allow distinguishing between different tracks in that set. | -| parent_id | JOINID(track.id) | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | ARGSETID | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| perf_session_id | LONG | The id of the perf session this counter was captured on. | -| cpu | LONG | The CPU the counter is associated with. Can be null if the counter is not associated with any CPU. | -| is_timebase | BOOL | Whether this counter is the sampling timebase for the session. | - -**counters** - -VIEW -Alias of the `counter` table. - -| Column | Type | Description | -|---|---|---| -| id | ID | Alias of `counter.id`. | -| ts | TIMESTAMP | Alias of `counter.ts`. | -| track_id | JOINID(track.id) | Alias of `counter.track_id`. | -| value | DOUBLE | Alias of `counter.value`. | -| arg_set_id | LONG | Alias of `counter.arg_set_id`. | -| name | STRING | Legacy column, should no longer be used. | -| unit | STRING | Legacy column, should no longer be used. | - -**cpu** - -VIEW -Contains information about the CPUs on the device this trace was taken on. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for this CPU. Identical to | -| ucpu | ID | Unique identifier for this CPU. Isn't equal to | -| cpu | LONG | The 0-based CPU core identifier. | -| cluster_id | LONG | The cluster id is shared by CPUs in the same cluster. | +| id | UINT | Unique identifier for this CPU. Identical to \|ucpu\|, prefer using \|ucpu\| instead. | +| ucpu | UINT | Unique identifier for this CPU. Isn't equal to \|cpu\| for remote machines and is equal to \|cpu\| for the host machine. | +| cpu | UINT | The 0-based CPU core identifier. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| cluster_id | UINT | The cluster id is shared by CPUs in the same cluster. | | processor | STRING | A string describing this core. | -| machine_id | LONG | Machine identifier, non-null for CPUs on a remote machine. | -| capacity | LONG | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: | -| arg_set_id | ARGSETID | Extra key/value pairs associated with this cpu. | +| machine_id | UINT | Machine identifier, non-null for CPUs on a remote machine. | +| capacity | UINT | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt | +| arg_set_id | UINT | Extra key/value pairs associated with this cpu. | -**cpu_available_frequencies** +
+ +
+ +
+ +**cpu_available_frequencies**. Contains the frequency values that the CPUs on the device are capable of running at. + +
VIEW Contains the frequency values that the CPUs on the device are capable of @@ -685,50 +115,75 @@ running at. | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this cpu frequency. | -| cpu | LONG | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| freq | LONG | CPU frequency in KHz. | -| ucpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| id | UINT | Unique identifier for this cpu frequency. | +| cpu | UINT | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| freq | UINT | CPU frequency in KHz. | +| ucpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -**sched** +
+ +
+ +
+ +**sched_slice**. This table holds slices with kernel thread scheduling information + +
VIEW -Contains scheduling slices with kernel thread scheduling information. -These slices are collected when the Linux "ftrace" data source is used with -the "sched/switch" and "sched/wakeup\*" events enabled. +This table holds slices with kernel thread scheduling information. These +slices are collected when the Linux "ftrace" data source is used with the +"sched/switch" and "sched/wakeup\*" events enabled. -The rows in this view will always have a matching row in the \|thread_state\| +The rows in this table will always have a matching row in the \|thread_state\| table with \|thread_state.state\| = 'Running' | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this scheduling slice. | -| ts | TIMESTAMP | The timestamp at the start of the slice. | -| dur | DURATION | The duration of the slice. | -| cpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | JOINID(thread.id) | The thread's unique id in the trace. | +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | | end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | -| priority | LONG | The kernel priority that the thread ran at. | -| ucpu | LONG | The unique CPU identifier that the slice executed on. | -| ts_end | LONG | Legacy column, should no longer be used. | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | -**sched_slice** +
+ +
+ +
+ +**sched** . Shorter alias for table `sched_slice`. + +
VIEW -Alias of `sched`. Prefer using `sched` instead. | Column | Type | Description | |---|---|---| -| id | ID | Alias of `sched.id`. | -| ts | TIMESTAMP | Alias of `sched.ts`. | -| dur | DURATION | Alias of `sched.dur`. | -| cpu | LONG | Alias of `sched.cpu`. | -| utid | JOINID(thread.id) | Alias of `sched.utid`. | -| end_state | STRING | Alias of `sched.end_state`. | -| priority | LONG | Alias of `sched.priority`. | -| ucpu | LONG | Alias of `sched.ucpu`. | +| id | UINT | Alias for `sched_slice.id`. | +| type | STRING | Alias for `sched_slice.type`. | +| ts | LONG | Alias for `sched_slice.ts`. | +| dur | LONG | Alias for `sched_slice.dur`. | +| cpu | UINT | Alias for `sched_slice.cpu`. | +| utid | UINT | Alias for `sched_slice.utid`. | +| end_state | STRING | Alias for `sched_slice.end_state`. | +| priority | INT | Alias for `sched_slice.priority`. | +| ucpu | UINT | Alias for `sched_slice.ucpu`. | +| ts_end | UINT | Legacy column, should no longer be used. | -**thread_state** +
+ +
+ +
+ +**thread_state**. This table contains the scheduling state of every thread on the system during the trace. The rows in this table which have \|state\| = 'Running', will have a corresponding row in the \|sched_slice\| table. + +
VIEW This table contains the scheduling state of every thread on the system during @@ -739,61 +194,378 @@ corresponding row in the \|sched_slice\| table. | Column | Type | Description | |---|---|---| -| id | ID | Unique identifier for this thread state. | -| ts | TIMESTAMP | The timestamp at the start of the slice. | -| dur | DURATION | The duration of the slice. | -| cpu | LONG | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | JOINID(thread.id) | The thread's unique id in the trace. | -| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in | -| io_wait | LONG | Indicates whether this thread was blocked on IO. | +| id | UINT | Unique identifier for this thread state. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in \|sched_slice.end_state\|. | +| io_wait | UINT | Indicates whether this thread was blocked on IO. | | blocked_function | STRING | The function in the kernel this thread was blocked on. | -| waker_utid | JOINID(thread.id) | The unique thread id of the thread which caused a wakeup of this thread. | -| waker_id | JOINID(thread_state.id) | The unique thread state id which caused a wakeup of this thread. | -| irq_context | LONG | Whether the wakeup was from interrupt context or process context. | -| ucpu | LONG | The unique CPU identifier that the thread executed on. | +| waker_utid | UINT | The unique thread id of the thread which caused a wakeup of this thread. | +| waker_id | UINT | The unique thread state id which caused a wakeup of this thread. | +| irq_context | UINT | Whether the wakeup was from interrupt context or process context. | +| ucpu | UINT | The unique CPU identifier that the thread executed on. | -**trace_metrics** +
+ +
+ +
+ +**raw**. Contains 'raw' events from the trace for some types of events + +
VIEW -Lists all metrics built-into trace processor. +Contains 'raw' events from the trace for some types of events. This table +only exists for debugging purposes and should not be relied on in production +usecases (i.e. metrics, standard library etc.) | Column | Type | Description | |---|---|---| -| name | STRING | The name of the metric. | +| id | UINT | Unique identifier for this raw event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | -**trace_bounds** +
+ +
+ +
+ +**ftrace_event**. Contains all the ftrace events in the trace + +
VIEW -Definition of `trace_bounds` table. The values are being filled by Trace -Processor when parsing the trace. -It is recommended to depend on the `trace_start()` and `trace_end()` -functions rather than directly on `trace_bounds`. +Contains all the ftrace events in the trace. This table exists only for +debugging purposes and should not be relied on in production usecases (i.e. +metrics, standard library etc). Note also that this table might be empty if +raw ftrace parsing has been disabled. | Column | Type | Description | |---|---|---| -| start_ts | TIMESTAMP | First ts in the trace. | -| end_ts | TIMESTAMP | End of the trace. | +| id | UINT | Unique identifier for this ftrace event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | + +
+ +
+ +
+ +**experimental_sched_upid**. The sched_slice table with the upid column. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | +| upid | UINT | The process's unique id in the trace. | + +
+ +
+ +
+ +**cpu_track**. Tracks which are associated to a single CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**cpu_counter_track**. Tracks containing counter-like events associated to a CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu counter track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**counters** . Alias of the `counter` table. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias of `counter.id`. | +| type | STRING | Alias of `counter.type`. | +| ts | LONG | Alias of `counter.ts`. | +| track_id | INT | Alias of `counter.track_id`. | +| value | DOUBLE | Alias of `counter.value`. | +| arg_set_id | INT | Alias of `counter.arg_set_id`. | +| name | STRING | Legacy column, should no longer be used. | +| unit | STRING | Legacy column, should no longer be used. | +| description | STRING | Legacy column, should no longer be used. | + +
+ +
+ +
+ +**slice**. Contains slices from userspace which explains what threads were doing during the trace. + +
+ +VIEW +Contains slices from userspace which explains what threads were doing +during the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | LONG | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | LONG | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_ts | LONG | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| cat | STRING | Alias of `category`. | +| slice_id | LONG | Alias of `id`. | + +
+ +
+ +
+ +**instant**. Contains instant events from userspace which indicates what happened at a single moment in time. + +
+ +VIEW +Contains instant events from userspace which indicates what happened at a +single moment in time. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | The timestamp of the instant (in nanoseconds). | +| track_id | INT | The id of the track this instant is located on. | +| name | STRING | The name of the instant. The name describes what happened during the instant. | +| arg_set_id | INT | The id of the argument set associated with this instant. | + +
+ +
+ +
+ +**slices** . Alternative alias of table `slice`. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Alias of `slice.id`. | +| type | STRING | Alias of `slice.type`. | +| ts | LONG | Alias of `slice.ts`. | +| dur | LONG | Alias of `slice.dur`. | +| track_id | INT | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | INT | Alias of `slice.depth`. | +| stack_id | LONG | Alias of `slice.stack_id`. | +| parent_stack_id | LONG | Alias of `slice.parent_stack_id`. | +| parent_id | INT | Alias of `slice.parent_id`. | +| arg_set_id | INT | Alias of `slice.arg_set_id`. | +| thread_ts | LONG | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | +| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | +| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | +| cat | LONG | Alias of `slice.cat`. | +| slice_id | LONG | Alias of `slice.slice_id`. | + +
+ +
+ +
+ +**thread**. Contains information of threads seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the thread. Prefer using `utid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| utid | INT | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| tid | INT | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|utid\| instead. | +| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| upid | LONG | The process hosting this thread. | +| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | +| machine_id | INT | Machine identifier, non-null for threads on a remote machine. | + +
+ +
+ +
+ +**process**. Contains information of processes seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the process. Prefer using `upid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| upid | LONG | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|upid\| instead. | +| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| parent_upid | INT | The upid of the process which caused this process to be spawned. | +| uid | INT | The Unix user id of the process. | +| android_appid | INT | Android appid of this process. | +| cmdline | STRING | /proc/cmdline for this process. | +| arg_set_id | INT | Extra args for this process. | +| machine_id | INT | Machine identifier, non-null for processes on a remote machine. | + +
+ +
+ +
+ +**args**. Arbitrary key-value pairs which allow adding metadata to other, strongly typed tables. Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| will be non-null. + +
+ +VIEW +Arbitrary key-value pairs which allow adding metadata to other, strongly +typed tables. +Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| +will be non-null. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the arg. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| arg_set_id | INT | The id for a single set of arguments. | +| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | +| key | STRING | The key for the arg. | +| int_value | LONG | The integer value of the arg. | +| string_value | STRING | The string value of the arg. | +| real_value | DOUBLE | The double value of the arg. | +| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | +| display_value | STRING | The human-readable formatted value of the arg. | + +
+ +
+ +
+ +**perf_session**. Contains the Linux perf sessions in the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the perf session. Prefer using `perf_session_id` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| perf_session_id | INT | The id of the perf session. | +| cmdline | STRING | Command line used to collect the data. | + +
+ +
#### Functions -**trace_start** +
-Fetch start of the trace. -Returns TIMESTAMP: Start of the trace. +**slice_is_ancestor** -\> BOOL. Given two slice ids, returns whether the first is an ancestor of the second. -**trace_end** +
-Fetch end of the trace. -Returns TIMESTAMP: End of the trace. +
-**trace_dur** +
-Fetch duration of the trace. -Returns DURATION: Duration of the trace. - -**slice_is_ancestor** - -Given two slice ids, returns whether the first is an ancestor of the second. Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. | Argument | Type | Description | @@ -801,4240 +573,119 @@ Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. | ancestor_id | LONG | Id of the potential ancestor slice. | | descendant_id | LONG | Id of the potential descendant slice. | +
+ +
+ +
+ +**trace_start** -\> LONG. Fetch start of the trace. + +
+ +
+ +
+ +Returns LONG: Start of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_end** -\> LONG. Fetch end of the trace. + +
+ +
+ +
+ +Returns LONG: End of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_dur** -\> LONG. Fetch duration of the trace. + +
+ +
+ +
+ +Returns LONG: Duration of the trace in nanoseconds. + +
+ +
+ #### Macros -**cast_int** +
+ +**cast_int** . Casts \|value\| to INT. + +
+ +
+ +
-Casts \|value\| to INT. Returns: Expr, | Argument | Type | Description | |---|---|---| | value | Expr | Query or subquery that will be cast. | -**cast_double** +
+ +
+ +
+ +**cast_double** . Casts \|value\| to DOUBLE. + +
+ +
+ +
-Casts \|value\| to DOUBLE. Returns: Expr, | Argument | Type | Description | |---|---|---| | value | Expr | Query or subquery that will be cast. | -**cast_string** +
+ +
+ +
+ +**cast_string** . Casts \|value\| to STRING. + +
+ +
+ +
-Casts \|value\| to STRING. Returns: Expr, | Argument | Type | Description | |---|---|---| | value | Expr | Query or subquery that will be cast. | -## Package: chrome +
-### chrome.android_input - -#### Views/Tables - -**chrome_deliver_android_input_event** - -TABLE -DeliverInputEvent is the third step in the input pipeline. -It is responsible for routing the input events within browser process. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Touch move processing duration. | -| utid | LONG | Utid. | -| android_input_id | STRING | Input id (assigned by the system, used by InputReader and InputDispatcher) | - -**chrome_android_input** - -TABLE -Collects information about input reader, input dispatcher and -DeliverInputEvent steps for the given Android input id. - -| Column | Type | Description | -|---|---|---| -| android_input_id | STRING | Input id. | -| input_reader_processing_start_ts | TIMESTAMP | Input reader step start timestamp. | -| input_reader_processing_end_ts | TIMESTAMP | Input reader step end timestamp. | -| input_reader_utid | LONG | Input reader step utid. | -| input_dispatcher_processing_start_ts | TIMESTAMP | Input dispatcher step start timestamp. | -| input_dispatcher_processing_end_ts | TIMESTAMP | Input dispatcher step end timestamp. | -| input_dispatcher_utid | LONG | Input dispatcher step utid. | -| deliver_input_event_start_ts | TIMESTAMP | DeliverInputEvent step start timestamp. | -| deliver_input_event_end_ts | TIMESTAMP | DeliverInputEvent step end timestamp. | -| deliver_input_event_utid | LONG | DeliverInputEvent step utid. | - -### chrome.chrome_scrolls - -#### Views/Tables - -**chrome_scroll_update_refs** - -TABLE -Ties together input (`LatencyInfo.Flow`) and frame (`Graphics.Pipeline`) -trace events. Only covers input events of the `GESTURE_SCROLL_UPDATE_EVENT` -type. - -| Column | Type | Description | -|---|---|---| -| scroll_update_latency_id | LONG | Id of the Chrome input pipeline (`LatencyInfo.Flow`). | -| touch_move_latency_id | LONG | Id of the touch move input corresponding to this scroll update. | -| presentation_latency_id | LONG | Id of the `EventLatency` of the frame that the input was presented in. | -| surface_frame_id | LONG | Id of the frame pipeline (`Graphics.Pipeline`), pre-surface aggregation. | -| display_trace_id | LONG | Id of the frame pipeline (`Graphics.Pipeline`), post-surface aggregation. | - -**chrome_scroll_update_input_pipeline** - -TABLE -Timestamps and durations for the input-associated (before coalescing inputs -into a frame) stages of a scroll. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | -| scroll_id | LONG | Id of the scroll this scroll update belongs to. | -| presented_in_frame_id | LONG | Id of the frame that this input was presented in. Can be joined with `chrome_scroll_update_frame_pipeline.id`. | -| is_presented | BOOL | Whether this input event was presented. | -| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | -| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). If this is `true`, "generation" and "touch_move" related timestamps and durations will be null. | -| is_first_scroll_update_in_scroll | BOOL | Whether this is the first update in a scroll. First scroll update can never be janky. | -| is_first_scroll_update_in_frame | BOOL | Whether this is the first input that was presented in frame `presented_in_frame_id`. | -| generation_ts | TIMESTAMP | Input generation timestamp (from the Android system). | -| input_reader_processing_end_ts | TIMESTAMP | End timestamp for the InputReader step (see android_input.sql). Only populated when atrace 'input' category is enabled. | -| input_dispatcher_processing_end_ts | TIMESTAMP | End timestamp for the InputDispatcher step (see android_input.sql). Only populated when atrace 'input' category is enabled. | -| generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the input. | -| browser_utid | LONG | Utid for the browser main thread. | -| touch_move_received_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | -| touch_move_received_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | -| touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event. | -| scroll_update_created_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | -| scroll_update_created_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | -| scroll_update_processing_dur | DURATION | Duration for creating a `GestureScrollUpdate` from a `TouchMove` event. | -| scroll_update_created_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the above. | -| browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch. | -| compositor_utid | LONG | Utid for the renderer compositor thread. | -| compositor_dispatch_slice_id | LONG | Slice id for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | -| compositor_dispatch_ts | TIMESTAMP | Timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice or the containing task (if available). | -| compositor_dispatch_dur | DURATION | Duration for the compositor dispatch itself. | -| compositor_dispatch_end_ts | TIMESTAMP | End timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | -| compositor_dispatch_to_coalesced_input_handled_dur | DURATION | Duration between compositor dispatch and coalescing input. | -| compositor_coalesced_input_handled_slice_id | LONG | Slice id for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | -| compositor_coalesced_input_handled_ts | TIMESTAMP | Timestamp for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | -| compositor_coalesced_input_handled_dur | DURATION | Duration for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | -| compositor_coalesced_input_handled_end_ts | TIMESTAMP | End timestamp for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | - -**chrome_scroll_update_frame_pipeline** - -TABLE -Timestamps and durations for the frame-associated (after coalescing inputs -into a frame) stages of a scroll. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | -| display_trace_id | LONG | Id of the aggregated frame this scroll update was presented in. | -| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | -| compositor_resample_slice_id | LONG | Slice id for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | -| compositor_resample_ts | TIMESTAMP | Timestamp for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | -| compositor_receive_begin_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_BEGIN_FRAME` slice or the containing task (if available). | -| compositor_generate_compositor_frame_slice_id | LONG | Slice id for the `STEP_GENERATE_COMPOSITOR_FRAME` slice. | -| compositor_generate_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_GENERATE_COMPOSITOR_FRAME` slice or the containing task (if available). | -| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between generating and submitting the compositor frame. | -| compositor_submit_compositor_frame_slice_id | LONG | Slice id for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_submit_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_submit_frame_dur | DURATION | Duration for submitting the compositor frame (to viz). | -| compositor_submit_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | -| viz_compositor_utid | LONG | Utid for the viz compositor thread. | -| viz_receive_compositor_frame_slice_id | LONG | Slice id for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | -| viz_receive_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice or the containing task (if available). | -| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | -| viz_receive_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | -| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | -| viz_draw_and_swap_slice_id | LONG | Slice id for the `STEP_DRAW_AND_SWAP` slice. | -| viz_draw_and_swap_ts | TIMESTAMP | Timestamp for the `STEP_DRAW_AND_SWAP` slice or the containing task (if available). | -| viz_draw_and_swap_dur | DURATION | Duration for the viz drawing/swapping work for this frame. | -| viz_send_buffer_swap_slice_id | LONG | Slice id for the `STEP_SEND_BUFFER_SWAP` slice. | -| viz_send_buffer_swap_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_BUFFER_SWAP` slice. | -| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | -| viz_gpu_thread_utid | LONG | Utid for the viz `CompositorGpuThread`. | -| viz_swap_buffers_slice_id | LONG | Slice id for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | -| viz_swap_buffers_ts | TIMESTAMP | Timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice or the containing task (if available). | -| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | -| viz_swap_buffers_end_ts | TIMESTAMP | End timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | -| viz_swap_buffers_to_latch_dur | DURATION | Duration of `EventLatency`'s `BufferReadyToLatch` step. | -| latch_timestamp | TIMESTAMP | Timestamp for `EventLatency`'s `LatchToSwapEnd` step. | -| viz_latch_to_presentation_dur | DURATION | Duration of either `EventLatency`'s `LatchToSwapEnd` + `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation` step. | -| presentation_timestamp | TIMESTAMP | Presentation timestamp for the frame. | - -**chrome_scrolls** - -TABLE -Defines slices for all of the individual scrolls in a trace based on the -LatencyInfo-based scroll definition. - -NOTE: this view of top level scrolls is based on the LatencyInfo definition -of a scroll, which differs subtly from the definition based on -EventLatencies. -TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ -WebView instances. Currently gesture_scroll_id unique within an instance, but -is not unique across multiple instances. Switching to an EventLatency based -definition of scrolls should resolve this. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The unique identifier of the scroll. | -| ts | TIMESTAMP | The start timestamp of the scroll. | -| dur | DURATION | The duration of the scroll. | -| gesture_scroll_begin_ts | TIMESTAMP | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | -| gesture_scroll_end_ts | TIMESTAMP | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | - -**chrome_scroll_update_info** - -TABLE -Timestamps and durations for the critical path stages during scrolling. -This table covers both the input-associated (before coalescing inputs into a -frame) and frame-associated (after coalescing inputs into a frame) stages of -a scroll: - -`... -| -+---+---+ -| | -V V` +---+ +---+ -\| *scroll_update_INPUT* \| \| *scroll_update_FRAME* \| -\| timestamps_and_metadata \| \| timestamps_and_metadata \| -+---+---+ +---+---+ -\| \| -V V -+---+ +---+ -\| chrome_scroll*update* \| \| chrome_scroll*update* \| -\| INPUT_pipeline \| \| FRAME_pipeline \| -+---+---+ +---+---+ -\| \| -+---+---+ -\| -V -+---+ -\| chrome_scroll_update_info \| -+---+ - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id of the `LatencyInfo.Flow` slices corresponding to this scroll event. | -| scroll_id | LONG | Id of the scroll this scroll update belongs to. | -| previous_input_id | LONG | Id (`LatencyInfo.ID`) of the previous input in this scroll. | -| frame_display_id | LONG | Id (`display_trace_id`) of the aggregated frame which this scroll update was presented in. | -| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | -| is_presented | BOOL | Whether this input event was presented. | -| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | -| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). If this is `true`, "generation" and "touch_move" related timestamps and durations will be null. | -| is_first_scroll_update_in_scroll | BOOL | Whether this is the first update in a scroll. First scroll update can never be janky. | -| is_first_scroll_update_in_frame | BOOL | Whether this is the first input that was presented in the frame. | -| browser_uptime_dur | DURATION | Duration from the start of the browser process to the first input generation timestamp. | -| generation_ts | TIMESTAMP | Input generation timestamp (from the Android system). | -| input_reader_dur | DURATION | Duration from the generation timestamp to the end of InputReader's work. Only populated when atrace 'input' category is enabled. | -| input_dispatcher_dur | DURATION | Duration of InputDispatcher's work. Only populated when atrace 'input' category is enabled. | -| since_previous_generation_dur | DURATION | Duration from the generation timestamp for the previous input to this input's generation timestamp. | -| generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the input. | -| browser_utid | LONG | Utid for the browser main thread. | -| touch_move_received_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | -| touch_move_received_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the touch move. | -| touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event. | -| scroll_update_created_slice_id | LONG | Slice id for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | -| scroll_update_created_ts | TIMESTAMP | Timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the gesture scroll. | -| scroll_update_processing_dur | DURATION | Duration for creating a `GestureScrollUpdate` from a `TouchMove` event. | -| scroll_update_created_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_INPUT_EVENT_UI` slice for the above. | -| browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch. | -| compositor_utid | LONG | Utid for the renderer compositor thread. | -| compositor_dispatch_slice_id | LONG | Slice id for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | -| compositor_dispatch_ts | TIMESTAMP | Timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice or the containing task (if available). | -| compositor_dispatch_dur | DURATION | Duration for the compositor dispatch itself. | -| compositor_dispatch_end_ts | TIMESTAMP | End timestamp for the `STEP_HANDLE_INPUT_EVENT_IMPL` slice. | -| compositor_dispatch_to_on_begin_frame_delay_dur | DURATION | Duration between compositor dispatch and input resampling work. | -| compositor_resample_slice_id | LONG | Slice id for the `STEP_RESAMPLE_SCROLL_EVENTS` slice. | -| compositor_coalesced_input_handled_slice_id | LONG | Slice id for the `STEP_DID_HANDLE_INPUT_AND_OVERSCROLL` slice. | -| compositor_on_begin_frame_ts | TIMESTAMP | Start timestamp for work done on the input during "OnBeginFrame". | -| compositor_on_begin_frame_dur | DURATION | Duration of the "OnBeginFrame" work for this input. | -| compositor_on_begin_frame_end_ts | TIMESTAMP | End timestamp for work done on the input during "OnBeginFrame". | -| compositor_on_begin_frame_to_generation_delay_dur | DURATION | Delay until the compositor work for generating the frame begins. | -| compositor_generate_compositor_frame_slice_id | LONG | Slice id for the `STEP_GENERATE_COMPOSITOR_FRAME` slice. | -| compositor_generate_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_GENERATE_COMPOSITOR_FRAME` slice or the containing task (if available). | -| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between generating and submitting the compositor frame. | -| compositor_submit_compositor_frame_slice_id | LONG | Slice id for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_submit_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_submit_frame_dur | DURATION | Duration for submitting the compositor frame (to viz). | -| compositor_submit_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_SUBMIT_COMPOSITOR_FRAME` slice. | -| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | -| viz_compositor_utid | LONG | Utid for the viz compositor thread. | -| viz_receive_compositor_frame_slice_id | LONG | Slice id for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | -| viz_receive_compositor_frame_ts | TIMESTAMP | Timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice or the containing task (if available). | -| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | -| viz_receive_compositor_frame_end_ts | TIMESTAMP | End timestamp for the `STEP_RECEIVE_COMPOSITOR_FRAME` slice. | -| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | -| viz_draw_and_swap_slice_id | LONG | Slice id for the `STEP_DRAW_AND_SWAP` slice. | -| viz_draw_and_swap_ts | TIMESTAMP | Timestamp for the `STEP_DRAW_AND_SWAP` slice or the containing task (if available). | -| viz_draw_and_swap_dur | DURATION | Duration for the viz drawing/swapping work for this frame. | -| viz_send_buffer_swap_slice_id | LONG | Slice id for the `STEP_SEND_BUFFER_SWAP` slice. | -| viz_send_buffer_swap_end_ts | TIMESTAMP | End timestamp for the `STEP_SEND_BUFFER_SWAP` slice. | -| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | -| viz_gpu_thread_utid | LONG | Utid for the viz `CompositorGpuThread`. | -| viz_swap_buffers_slice_id | LONG | Slice id for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | -| viz_swap_buffers_ts | TIMESTAMP | Timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice or the containing task (if available). | -| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | -| viz_swap_buffers_end_ts | TIMESTAMP | End timestamp for the `STEP_BUFFER_SWAP_POST_SUBMIT` slice. | -| viz_swap_buffers_to_latch_dur | DURATION | Duration of `EventLatency`'s `BufferReadyToLatch` step. | -| latch_timestamp | TIMESTAMP | Timestamp for `EventLatency`'s `LatchToSwapEnd` step. | -| viz_latch_to_presentation_dur | DURATION | Duration of either `EventLatency`'s `LatchToSwapEnd` + `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation` step. | -| presentation_timestamp | TIMESTAMP | Presentation timestamp for the frame. | - -**chrome_scroll_frame_info** - -TABLE -A list of all presented Chrome frames which contain scroll updates and associated -metadata. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id (frame's display_trace_id) for the given frame. | -| scroll_id | LONG | Id of the scroll this scroll update belongs to. | -| last_input_before_this_frame_id | LONG | Id (LatencyInfo.ID) of the last input before this frame. | -| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). TODO(b/394303662): Remove in favour of `vsync_interval_dur`. | -| vsync_interval_dur | DURATION | Vsync interval (in nanoseconds). | -| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. This comes directly from `perfetto.protos.EventLatency.is_janky_scrolled_frame`. | -| is_inertial | BOOL | Whether the corresponding scroll is inertial (fling). | -| total_input_delta_y | DOUBLE | Sum of all input deltas for all scroll updates in this frame. These values are based on the delta of the OS input events. | -| presented_scrolled_delta_y | DOUBLE | Presented delta (change in page offset) for the given frame. This delta is computed by Chrome (based on the input events). | -| browser_uptime_dur | DURATION | Duration from the start of the browser process to the first input generation timestamp. | -| first_input_generation_ts | TIMESTAMP | Input generation timestamp (from the Android system) for the first input. | -| input_reader_dur | DURATION | Duration from the generation timestamp to the end of InputReader's work. Only populated when atrace 'input' category is enabled. | -| input_dispatcher_dur | DURATION | Duration of InputDispatcher's work. Only populated when atrace 'input' category is enabled. | -| previous_last_input_to_first_input_generation_dur | DURATION | Duration from the previous input (last input that wasn't part of this frame) to the first input in this frame. | -| presentation_ts | TIMESTAMP | Presentation timestamp for the frame. | -| browser_utid | JOINID(thread.id) | Utid for the browser main thread. | -| first_input_generation_to_browser_main_dur | DURATION | Duration from input generation to when the browser received the first input in this frame. | -| first_input_generation_to_browser_main_delta_dur | DURATION | Difference between `first_input_generation_to_browser_main_dur` for this frame and the previous frame in the same scroll. | -| first_input_touch_move_processing_dur | DURATION | Duration for processing a `TouchMove` event for the first input in this frame. | -| first_input_touch_move_processing_delta_dur | DURATION | Difference between `first_input_touch_move_processing_dur` for this frame and the previous frame in the same scroll. | -| compositor_utid | JOINID(thread.id) | Utid for the renderer compositor thread. | -| first_input_browser_to_compositor_delay_dur | DURATION | Duration between the browser and compositor dispatch for the first input in this frame. | -| first_input_browser_to_compositor_delay_delta_dur | DURATION | Difference between `first_input_browser_to_compositor_delay_dur` for this frame and the previous frame in the same scroll. | -| first_input_compositor_dispatch_dur | DURATION | Duration for the compositor dispatch for the first input in this frame. | -| first_input_compositor_dispatch_delta_dur | DURATION | Difference between `first_input_compositor_dispatch_dur` for this frame and the previous frame in the same scroll. | -| first_input_compositor_dispatch_to_on_begin_frame_delay_dur | DURATION | Duration between the compositor dispatch and the "OnBeginFrame" work for the first input in this frame. | -| first_input_compositor_dispatch_to_on_begin_frame_delay_delta_dur | DURATION | Difference between `first_input_compositor_dispatch_to_on_begin_frame_delay_dur` for this frame and the previous frame in the same scroll. | -| compositor_on_begin_frame_dur | DURATION | Duration of the "OnBeginFrame" work for this frame. | -| compositor_on_begin_frame_delta_dur | DURATION | Difference between `compositor_on_begin_frame_dur` for this frame and the previous frame in the same scroll. | -| compositor_on_begin_frame_to_generation_delay_dur | DURATION | Duration between the "OnBeginFrame" work and the generation of this frame. | -| compositor_on_begin_frame_to_generation_delay_delta_dur | DURATION | Difference between `compositor_on_begin_frame_to_generation_delay_dur` for this frame and the previous frame in the same scroll. | -| compositor_generate_frame_to_submit_frame_dur | DURATION | Duration between the generation and submission of this frame. | -| compositor_generate_frame_to_submit_frame_delta_dur | DURATION | Difference between `compositor_generate_frame_to_submit_frame_dur` for this frame and the previous frame in the same scroll. | -| compositor_submit_frame_dur | DURATION | Duration for submitting this frame. | -| compositor_submit_frame_delta_dur | DURATION | Difference between `compositor_submit_frame_dur` for this frame and the previous frame in the same scroll. | -| viz_compositor_utid | JOINID(thread.id) | Utid for the viz compositor thread. | -| compositor_to_viz_delay_dur | DURATION | Delay when a compositor frame is sent from the renderer to viz. | -| compositor_to_viz_delay_delta_dur | DURATION | Difference between `compositor_to_viz_delay_dur` for this frame and the previous frame in the same scroll. | -| viz_receive_compositor_frame_dur | DURATION | Duration of the viz work done on receiving the compositor frame. | -| viz_receive_compositor_frame_delta_dur | DURATION | Difference between `viz_receive_compositor_frame_dur` for this frame and the previous frame in the same scroll. | -| viz_wait_for_draw_dur | DURATION | Duration between viz receiving the compositor frame to frame draw. | -| viz_wait_for_draw_delta_dur | DURATION | Difference between `viz_wait_for_draw_dur` for this frame and the previous frame in the same scroll. | -| viz_draw_and_swap_dur | DURATION | Duration of the viz drawing/swapping work for this frame. | -| viz_draw_and_swap_delta_dur | DURATION | Difference between `viz_draw_and_swap_dur` for this frame and the previous frame in the same scroll. | -| viz_gpu_thread_utid | JOINID(thread.id) | Utid for the viz `CompositorGpuThread`. | -| viz_to_gpu_delay_dur | DURATION | Delay between viz work on compositor thread and `CompositorGpuThread`. | -| viz_to_gpu_delay_delta_dur | DURATION | Difference between `viz_to_gpu_delay_dur` for this frame and the previous frame in the same scroll. | -| viz_swap_buffers_dur | DURATION | Duration of frame buffer swapping work on viz. | -| viz_swap_buffers_delta_dur | DURATION | Difference between `viz_swap_buffers_dur` for this frame and the previous frame in the same scroll. | -| viz_swap_buffers_to_latch_dur | DURATION | Time between buffers ready until Choreographer's latch. | -| viz_swap_buffers_to_latch_delta_dur | DURATION | Difference between `viz_swap_buffers_to_latch_dur` for this frame and the previous frame in the same scroll. | -| viz_latch_to_presentation_dur | DURATION | Duration between Choreographer's latch and presentation. | -| viz_latch_to_presentation_delta_dur | DURATION | Difference between `viz_latch_to_presentation_dur` for this frame and the previous frame in the same scroll. | - -**chrome_scroll_update_info_step_templates** - -TABLE -Source of truth for the definition of the stages of a scroll. Mainly intended -for visualization purposes (e.g. in Chrome Scroll Jank plugin). - -| Column | Type | Description | -|---|---|---| -| step_name | STRING | The name of a stage of a scroll. | -| ts_column_name | STRING | The name of the column in `chrome_scroll_update_info` which contains the timestamp of the stage. | -| dur_column_name | STRING | The name of the column in `chrome_scroll_update_info` which contains the duration of the stage. NULL if the stage doesn't have a duration. | - -### chrome.event_latency - -#### Views/Tables - -**chrome_event_latencies** - -TABLE -All EventLatency slices. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice Id for the EventLatency scroll event. | -| name | STRING | Slice name. | -| ts | TIMESTAMP | The start timestamp of the scroll. | -| dur | DURATION | The duration of the scroll. | -| scroll_update_id | LONG | The id of the scroll update event (aka LatencyInfo.ID). | -| surface_frame_trace_id | LONG | The id of the first frame (pre-surface aggregation) which included the scroll update and was presented. NULL if: (1) the event is not a scroll update (`event_type` is NOT GESTURE_SCROLL_UPDATE, FIRST_GESTURE_SCROLL_UPDATE, or INERTIAL_GESTURE_SCROLL_UPDATE), (2) the scroll update wasn't presented (e.g. it was an overscroll) or (3) the trace comes from an old Chrome version ( was first included in version 134.0.6977.0 and was cherry-picked in version 133.0.6943.33). | -| display_trace_id | LONG | The id of the first frame (post-surface aggregation) which included the scroll update and was presented. NULL if: (1) the event is not a scroll update (`event_type` is NOT GESTURE_SCROLL_UPDATE, FIRST_GESTURE_SCROLL_UPDATE, or INERTIAL_GESTURE_SCROLL_UPDATE), (2) the scroll update wasn't presented (e.g. it was an overscroll) or (3) the trace comes from an old Chrome version ( was first included in version 134.0.6977.0 and was cherry-picked in version 133.0.6943.33). | -| is_presented | BOOL | Whether this input event was presented. | -| event_type | STRING | EventLatency event type. | -| track_id | LONG | Perfetto track this slice is found on. | -| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | -| is_janky_scrolled_frame | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. | -| buffer_available_timestamp | LONG | Timestamp of the BufferAvailableToBufferReady substage. | -| buffer_ready_timestamp | LONG | Timestamp of the BufferReadyToLatch substage. | -| latch_timestamp | LONG | Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a fallback). | -| swap_end_timestamp | LONG | Timestamp of the SwapEndToPresentationCompositorFrame substage. | -| presentation_timestamp | LONG | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | - -**chrome_gesture_scroll_updates** - -TABLE -All scroll-related events (frames) including gesture scroll updates, begins -and ends with respective scroll ids and start/end timestamps, regardless of -being presented. This includes pinches that were presented. See b/315761896 -for context on pinches. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice Id for the EventLatency scroll event. | -| name | STRING | Slice name. | -| ts | TIMESTAMP | The start timestamp of the scroll. | -| dur | DURATION | The duration of the scroll. | -| scroll_update_id | LONG | The id of the scroll update event. | -| is_presented | BOOL | Whether this input event was presented. | -| event_type | STRING | EventLatency event type. | -| track_id | LONG | Perfetto track this slice is found on. | -| vsync_interval_ms | DOUBLE | Vsync interval (in milliseconds). | -| is_janky | BOOL | Whether the corresponding frame is janky based on the Event.ScrollJank.DelayedFramesPercentage.FixedWindow metric. | -| buffer_available_timestamp | LONG | Timestamp of the BufferAvailableToBufferReady substage. | -| buffer_ready_timestamp | LONG | Timestamp of the BufferReadyToLatch substage. | -| latch_timestamp | LONG | Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a fallback). | -| swap_end_timestamp | LONG | Timestamp of the SwapEndToPresentationCompositorFrame substage. | -| presentation_timestamp | LONG | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | -| scroll_id | LONG | The id of the scroll. | - -### chrome.event_latency_description - -#### Views/Tables - -**chrome_event_latency_stage_descriptions** - -TABLE -Source of truth of the descriptions of EventLatency stages. - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the EventLatency stage. | -| description | STRING | A description of the EventLatency stage. | - -### chrome.graphics_pipeline - -#### Views/Tables - -**chrome_graphics_pipeline_surface_frame_steps** - -TABLE -`Graphics.Pipeline` steps corresponding to work done by a Viz client to -produce a frame (i.e. before surface aggregation). Covers steps: - -- STEP_ISSUE_BEGIN_FRAME -- STEP_RECEIVE_BEGIN_FRAME -- STEP_GENERATE_RENDER_PASS -- STEP_GENERATE_COMPOSITOR_FRAME -- STEP_SUBMIT_COMPOSITOR_FRAME -- STEP_RECEIVE_COMPOSITOR_FRAME -- STEP_RECEIVE_BEGIN_FRAME_DISCARD -- STEP_DID_NOT_PRODUCE_FRAME -- STEP_DID_NOT_PRODUCE_COMPOSITOR_FRAME - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice Id of the `Graphics.Pipeline` slice. | -| ts | TIMESTAMP | The start timestamp of the slice/step. | -| dur | DURATION | The duration of the slice/step. | -| step | STRING | Step name of the `Graphics.Pipeline` slice. | -| surface_frame_trace_id | LONG | Id of the graphics pipeline, pre-surface aggregation. | -| utid | LONG | Utid of the thread where this slice exists. | -| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | - -**chrome_graphics_pipeline_display_frame_steps** - -TABLE -`Graphics.Pipeline` steps corresponding to work done on creating and -presenting one frame during/after surface aggregation. Covers steps: - -- STEP_DRAW_AND_SWAP -- STEP_SURFACE_AGGREGATION -- STEP_SEND_BUFFER_SWAP -- STEP_BUFFER_SWAP_POST_SUBMIT -- STEP_FINISH_BUFFER_SWAP -- STEP_SWAP_BUFFERS_ACK - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice Id of the `Graphics.Pipeline` slice. | -| ts | TIMESTAMP | The start timestamp of the slice/step. | -| dur | DURATION | The duration of the slice/step. | -| step | STRING | Step name of the `Graphics.Pipeline` slice. | -| display_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | -| utid | LONG | Utid of the thread where this slice exists. | -| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | - -**chrome_surface_frame_id_to_first_display_id** - -TABLE -Links surface frames (`chrome_graphics_pipeline_surface_frame_steps`) to the -the first display frame (`chrome_graphics_pipeline_display_frame_steps`) into -which it was included. As an display frame usually aggregates frames from -multiple surfaces, multiple `surface_frame_trace_id`s will correspond to one -`display_trace_id`. - -| Column | Type | Description | -|---|---|---| -| surface_frame_trace_id | LONG | Id of the graphics pipeline, pre-surface aggregation. | -| display_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | - -**chrome_graphics_pipeline_inputs_to_surface_frames** - -TABLE -Links inputs (`chrome_input_pipeline_steps.latency_id`) to the surface frame -(`chrome_graphics_pipeline_surface_frame_steps`) to which they correspond. -In other words, in general, multiple `latency_id`s will correspond to one -`surface_frame_trace_id`. - -| Column | Type | Description | -|---|---|---| -| latency_id | LONG | Id corresponding to the input pipeline. | -| surface_frame_trace_id | LONG | Id of the graphics pipeline, post-surface aggregation. | - -### chrome.histograms - -#### Views/Tables - -**chrome_histograms** - -TABLE -A helper view on top of the histogram events emitted by Chrome. -Requires "disabled-by-default-histogram_samples" Chrome category or the -"org.chromium.histogram_sample" data source. - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the histogram. | -| value | LONG | The value of the histogram sample. | -| ts | TIMESTAMP | Alias of | -| thread_name | STRING | Thread name. | -| utid | LONG | Utid of the thread. | -| tid | LONG | Tid of the thread. | -| process_name | STRING | Process name. | -| upid | LONG | Upid of the process. | -| pid | LONG | Pid of the process. | - -### chrome.input - -#### Views/Tables - -**chrome_inputs** - -TABLE -Each row represents one input pipeline. - -| Column | Type | Description | -|---|---|---| -| latency_id | LONG | Id of this Chrome input pipeline (LatencyInfo). | -| input_type | STRING | Input type. | - -**chrome_input_pipeline_steps** - -TABLE -Since not all steps have associated input type (but all steps -for a given latency id should have the same input type), -populate input type for steps where it would be NULL. - -| Column | Type | Description | -|---|---|---| -| latency_id | LONG | Id of this Chrome input pipeline (LatencyInfo). | -| slice_id | LONG | Slice id | -| ts | TIMESTAMP | The step timestamp. | -| dur | DURATION | Step duration. | -| utid | LONG | Utid of the thread. | -| step | STRING | Step name (ChromeLatencyInfo.step). | -| input_type | STRING | Input type. | -| task_start_time_ts | TIMESTAMP | Start time of the parent Chrome scheduler task (if any) of this step. | - -**chrome_coalesced_inputs** - -TABLE -For each input, if it was coalesced into another input, get the other input's -latency id. - -| Column | Type | Description | -|---|---|---| -| coalesced_latency_id | LONG | The `latency_id` of the coalesced input. | -| presented_latency_id | LONG | The `latency_id` of the other input that the current input was coalesced into. Guaranteed to be different from `coalesced_latency_id`. | - -**chrome_touch_move_to_scroll_update** - -TABLE -Each scroll update event (except flings) in Chrome starts its life as a touch -move event, which is then eventually converted to a scroll update itself. -Each of these events is represented by its own LatencyInfo. This table -contains a mapping between touch move events and scroll update events they -were converted into. - -| Column | Type | Description | -|---|---|---| -| touch_move_latency_id | LONG | Latency id of the touch move input (LatencyInfo). | -| scroll_update_latency_id | LONG | Latency id of the corresponding scroll update input (LatencyInfo). | - -**chrome_dispatch_android_input_event_to_touch_move** - -TABLE -Matches Android input id to the corresponding touch move event. - -| Column | Type | Description | -|---|---|---| -| android_input_id | STRING | Input id (assigned by the system, used by InputReader and InputDispatcher) | -| touch_move_latency_id | LONG | Latency id. | - -### chrome.interactions - -#### Views/Tables - -**chrome_interactions** - -TABLE -All critical user interaction events, including type and table with -associated metrics. - -| Column | Type | Description | -|---|---|---| -| scoped_id | LONG | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | -| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | -| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | -| ts | TIMESTAMP | Timestamp of the CUI event. | -| dur | DURATION | Duration of the CUI event. | - -### chrome.loadline_2 - -#### Views/Tables - -**chrome_loadline2_stages** - -TABLE - -| Column | Type | Description | -|---|---|---| -| page | STRING | Name of the page | -| story_start | TIMESTAMP | Story start timestamp | -| start_request | TIMESTAMP | Start request timestamp | -| end_request | TIMESTAMP | End request timestamp | -| renderer_ready | TIMESTAMP | Renderer ready timestamp | -| visual_mark | TIMESTAMP | Visual mark timestamp | -| visual_raf | TIMESTAMP | Visual rAF timestamp | -| visual_presentation | TIMESTAMP | Visual presentation timestamp | -| interactive_mark | TIMESTAMP | Interactive mark timestamp | -| interactive_raf | TIMESTAMP | Interactive rAF timestamp | -| interactive_presentation | TIMESTAMP | Interactive presentation timestamp | -| story_finish | TIMESTAMP | Story finish timestamp | - -### chrome.metadata - -#### Functions - -**chrome_hardware_class** - -Returns hardware class of the device, often use to find device brand -and model. -Returns STRING: Hardware class name. - -### chrome.page_loads - -#### Views/Tables - -**chrome_page_loads** - -TABLE -Chrome page loads, including associated high-level metrics and properties. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | -| navigation_id | LONG | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | -| navigation_start_ts | TIMESTAMP | Timestamp of the start of navigation. | -| fcp | LONG | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | -| fcp_ts | TIMESTAMP | Timestamp of the first contentful paint. | -| lcp | LONG | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | -| lcp_ts | TIMESTAMP | Timestamp of the largest contentful paint. | -| dom_content_loaded_event_ts | TIMESTAMP | Timestamp of the DomContentLoaded event: | -| load_event_ts | TIMESTAMP | Timestamp of the window load event: | -| mark_fully_loaded_ts | TIMESTAMP | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | -| mark_fully_visible_ts | TIMESTAMP | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | -| mark_interactive_ts | TIMESTAMP | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | -| url | STRING | URL at the page load event. | -| browser_upid | LONG | The unique process id (upid) of the browser process where the page load occurred. | - -### chrome.scroll_jank.predictor_error - -#### Views/Tables - -**chrome_predictor_error** - -TABLE -The scrolling offsets and predictor jank values for the actual (applied) -scroll events. - -| Column | Type | Description | -|---|---|---| -| scroll_id | LONG | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | -| scroll_update_id | LONG | An ID that ties this | -| present_ts | TIMESTAMP | Presentation timestamp. | -| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | -| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | -| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | -| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | -| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | -| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | - -### chrome.scroll_jank.scroll_jank_cause_map - -#### Views/Tables - -**chrome_scroll_jank_cause_descriptions** - -TABLE -Source of truth of the descriptions of EventLatency-based scroll jank causes. - -| Column | Type | Description | -|---|---|---| -| event_latency_stage | STRING | The name of the EventLatency stage. | -| cause_process | STRING | The process where the cause of scroll jank occurred. | -| cause_thread | STRING | The thread where the cause of scroll jank occurred. | -| cause_description | STRING | A description of the cause of scroll jank. | - -**chrome_scroll_jank_causes_with_event_latencies** - -VIEW -Combined description of scroll jank cause and associated event latency stage. - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the EventLatency stage. | -| description | STRING | Description of the EventLatency stage. | -| cause_process | STRING | The process name that may cause scroll jank. | -| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | -| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | - -### chrome.scroll_jank.scroll_jank_cause_utils - -#### Table Functions - -**chrome_select_scroll_jank_cause_thread** - -Function to retrieve the thread id of the thread on a particular process if -there are any slices during a particular EventLatency slice duration; this -upid/thread combination refers to a cause of Scroll Jank. - -| Argument | Type | Description | -|---|---|---| -| event_latency_id | LONG | The slice id of an EventLatency slice. | -| process_type | STRING | The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. | -| thread_name | STRING | The name of the thread. | - -| Column | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | The utid associated with | - -### chrome.scroll_jank.scroll_jank_intervals - -#### Views/Tables - -**chrome_janky_event_latencies_v3** - -TABLE -Selects EventLatency slices that correspond with janks in a scroll. This is -based on the V3 version of scroll jank metrics. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The slice id. | -| ts | TIMESTAMP | The start timestamp of the slice. | -| dur | DURATION | The duration of the slice. | -| track_id | LONG | The track_id for the slice. | -| name | STRING | The name of the slice (EventLatency). | -| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | -| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | -| delayed_frame_count | LONG | How many vsyncs this frame missed its deadline by. | -| frame_jank_ts | TIMESTAMP | The start timestamp where frame presentation was delayed. | -| frame_jank_dur | LONG | The duration in ms of the delay in frame presentation. | - -**chrome_janky_frame_presentation_intervals** - -VIEW -Frame presentation interval is the delta between when the frame was supposed -to be presented and when it was actually presented. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Unique id. | -| ts | TIMESTAMP | The start timestamp of the slice. | -| dur | DURATION | The duration of the slice. | -| delayed_frame_count | LONG | How many vsyncs this frame missed its deadline by. | -| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | -| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | -| event_latency_id | LONG | The id of the associated event latency in the slice table. | - -**chrome_scroll_stats** - -TABLE -Scroll jank frame presentation stats for individual scrolls. - -| Column | Type | Description | -|---|---|---| -| scroll_id | LONG | Id of the individual scroll. | -| frame_count | LONG | The number of frames in the scroll. | -| missed_vsyncs | LONG | The number of missed vsyncs in the scroll. | -| presented_frame_count | LONG | The number presented frames in the scroll. | -| janky_frame_count | LONG | The number of janky frames in the scroll. | -| janky_frame_percent | DOUBLE | The % of frames that janked in the scroll. | - -**chrome_scroll_jank_intervals_v3** - -TABLE -Defines slices for all of janky scrolling intervals in a trace. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The unique identifier of the janky interval. | -| ts | TIMESTAMP | The start timestamp of the janky interval. | -| dur | DURATION | The duration of the janky interval. | - -### chrome.scroll_jank.scroll_jank_v3 - -#### Views/Tables - -**chrome_presented_gesture_scrolls** - -TABLE -Scroll updates, corresponding to all input events that were converted to a -presented scroll update. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Minimum slice id for input presented in this frame, the non-presented input. | -| ts | TIMESTAMP | The start timestamp for producing the frame. | -| dur | DURATION | The duration between producing and presenting the frame. | -| last_presented_input_ts | TIMESTAMP | The timestamp of the last input that arrived and got presented in the frame. | -| scroll_update_id | LONG | The id of the scroll update event, a unique identifier to the gesture. | -| scroll_id | LONG | The id of the ongoing scroll. | -| presentation_timestamp | LONG | Frame presentation timestamp. | -| event_type | STRING | EventLatency event type. | - -**chrome_scroll_updates_with_deltas** - -TABLE -Associate every trace_id with it's perceived delta_y on the screen after -prediction. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | LONG | The id of the scroll update event. | -| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | - -**chrome_full_frame_view** - -TABLE -Obtain the subset of input events that were fully presented. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the frame. | -| ts | TIMESTAMP | Start timestamp of the frame. | -| last_presented_input_ts | TIMESTAMP | The timestamp of the last presented input. | -| scroll_id | LONG | ID of the associated scroll. | -| scroll_update_id | LONG | ID of the associated scroll update. | -| event_latency_id | LONG | ID of the associated EventLatency. | -| dur | DURATION | Duration of the associated EventLatency. | -| presentation_timestamp | LONG | Frame presentation timestamp. | - -**chrome_full_frame_delta_view** - -TABLE -Join deltas with EventLatency data. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the frame. | -| ts | TIMESTAMP | Start timestamp of the frame. | -| scroll_id | LONG | ID of the associated scroll. | -| scroll_update_id | LONG | ID of the associated scroll update. | -| last_presented_input_ts | TIMESTAMP | The timestamp of the last presented input. | -| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | -| event_latency_id | LONG | ID of the associated EventLatency. | -| dur | DURATION | Duration of the associated EventLatency. | -| presentation_timestamp | LONG | Frame presentation timestamp. | - -**chrome_merged_frame_view** - -TABLE -Group all gestures presented at the same timestamp together in -a single row. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the frame. | -| max_start_ts | TIMESTAMP | The timestamp of the last presented input. | -| min_start_ts | TIMESTAMP | The earliest frame start timestamp. | -| scroll_id | LONG | ID of the associated scroll. | -| scroll_update_id | LONG | ID of the associated scroll update. | -| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | -| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | -| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | -| event_latency_id | LONG | ID of the associated EventLatency. | -| dur | DURATION | Maximum duration of the associated EventLatency. | -| presentation_timestamp | LONG | Frame presentation timestamp. | - -**chrome_frame_info_with_delay** - -TABLE -View contains all chrome presented frames during gesture updates -while calculating delay since last presented which usually should -equal to \|VSYNC_INTERVAL\| if no jank is present. - -| Column | Type | Description | -|---|---|---| -| id | LONG | gesture scroll slice id. | -| max_start_ts | TIMESTAMP | OS timestamp of the last touch move arrival within a frame. | -| min_start_ts | TIMESTAMP | OS timestamp of the first touch move arrival within a frame. | -| scroll_id | LONG | The scroll which the touch belongs to. | -| scroll_update_id | LONG | ID of the associated scroll update. | -| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | -| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | -| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives | -| event_latency_id | LONG | Event latency id of the presented frame. | -| dur | DURATION | Duration of the EventLatency. | -| presentation_timestamp | LONG | Timestamp at which the frame was shown on the screen. | -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals | -| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | -| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | - -**chrome_vsyncs** - -TABLE -Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the -minimum delay between frames larger than zero. - -TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. - -| Column | Type | Description | -|---|---|---| -| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | - -**chrome_janky_frames_no_cause** - -TABLE -Filter the frame view only to frames that had missed vsyncs. - -| Column | Type | Description | -|---|---|---| -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | -| event_latency_id | LONG | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | LONG | The scroll corresponding to this frame. | -| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | - -**chrome_janky_frames_no_subcause** - -TABLE -Janky frame information including the jank cause. - -| Column | Type | Description | -|---|---|---| -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | -| event_latency_id | LONG | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | LONG | The scroll corresponding to this frame. | -| prev_event_latency_id | LONG | The event latency id that will be used as a reference to determine the jank cause. | -| cause_id | LONG | Id of the slice corresponding to the offending stage. | - -**chrome_janky_frames** - -TABLE -Finds all causes of jank for all janky frames, and a cause of sub jank -if the cause of jank was GPU related. - -| Column | Type | Description | -|---|---|---| -| cause_of_jank | STRING | The reason the Vsync was missed. | -| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than | -| event_latency_id | LONG | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | LONG | The scroll corresponding to this frame. | - -**chrome_unique_frame_presentation_ts** - -TABLE -Counting all unique frame presentation timestamps. - -| Column | Type | Description | -|---|---|---| -| presentation_timestamp | LONG | The unique frame presentation timestamp. | - -**chrome_janky_frames_percentage** - -TABLE -Dividing missed frames over total frames to get janky frame percentage. -This represents the v3 scroll jank metrics. -Reflects Event.Jank.DelayedFramesPercentage UMA metric. - -| Column | Type | Description | -|---|---|---| -| delayed_frame_percentage | DOUBLE | The percent of missed frames relative to total frames - aka the percent of janky frames. | - -**chrome_frames_per_scroll** - -TABLE -Number of frames and janky frames per scroll. - -| Column | Type | Description | -|---|---|---| -| scroll_id | LONG | The ID of the scroll. | -| num_frames | LONG | The number of frames in the scroll. | -| num_janky_frames | LONG | The number of delayed/janky frames. | -| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | - -### chrome.scroll_jank.scroll_jank_v3_cause - -#### Functions - -**chrome_get_v3_jank_cause_id** - -Given two slice Ids A and B, find the maximum difference -between the durations of it's direct children with matching names -for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) -and slice B has children named (X, Y) with durations of (9, 9), the function will return -the slice id of the slice named Z that is A's child, as no matching slice named Z was found -under B, making 5 - 0 = 5 the maximum delta between both slice's direct children -Returns LONG: The slice id of the breakdown that has the maximum duration delta. - -| Argument | Type | Description | -|---|---|---| -| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | -| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to | - -### chrome.scroll_jank.scroll_offsets - -#### Views/Tables - -**chrome_scroll_input_deltas** - -TABLE -The raw input deltas for all input events which were part of a scroll. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | LONG | Scroll update id (aka LatencyInfo.ID) for this scroll update input event. | -| delta_x | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the X axis vs previous, as reported by the OS. | -| delta_y | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the Y axis vs previous, as reported by the OS. | - -**chrome_scroll_input_offsets** - -TABLE -The raw coordinates and pixel offsets for all input events which were part of -a scroll. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | LONG | An ID for this scroll update (aka LatencyInfo.ID). | -| scroll_id | LONG | An ID for the scroll this scroll update belongs to. | -| ts | TIMESTAMP | Timestamp the of the scroll input event. | -| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | -| relative_offset_y | DOUBLE | The total delta of all scroll updates within the same as scroll up to and including this scroll update. | - -**chrome_scroll_presented_deltas** - -TABLE -The page offset delta (by how much the page was scrolled vs previous frame) -for each frame. -This is the resulting delta that is shown to the user after the input has -been processed. `chrome_scroll_input_deltas` tracks the underlying signal -deltas between consecutive input events. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | LONG | Scroll update id (aka LatencyInfo.ID) for this scroll update input event. | -| delta_x | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the X axis vs previous, as reported by the OS. | -| delta_y | DOUBLE | The delta in pixels (scaled to the device's screen size) how much this input event moved over the Y axis vs previous, as reported by the OS. | -| offset_x | LONG | The page offset in pixels (scaled to the device's screen size) along the X axis. | -| offset_y | LONG | The page offset in pixels (scaled to the device's screen size) along the Y axis. | - -**chrome_presented_scroll_offsets** - -TABLE -The scrolling offsets for the actual (applied) scroll events. These are not -necessarily inclusive of all user scroll events, rather those scroll events -that are actually processed. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | LONG | An ID for this scroll update (aka LatencyInfo.ID). | -| scroll_id | LONG | An ID for the scroll this scroll update belongs to. | -| ts | TIMESTAMP | Presentation timestamp. | -| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | -| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | - -### chrome.scroll_jank.utils - -#### Table Functions - -**chrome_select_long_task_slices** - -Extract mojo information for the long-task-tracking scenario for specific -names. For example, LongTaskTracker slices may have associated IPC -metadata, or InterestingTask slices for input may have associated IPC to -determine whether the task is fling/etc. - -| Argument | Type | Description | -|---|---|---| -| name | STRING | The name of slice. | - -| Column | Type | Description | -|---|---|---| -| interface_name | STRING | Name of the interface of the IPC call. | -| ipc_hash | LONG | Hash of the IPC call. | -| message_type | STRING | Message type (e.g. reply). | -| id | LONG | The slice id. | - -### chrome.scroll_jank_tagging - -#### Views/Tables - -**chrome_scroll_jank_tags** - -TABLE -List of scroll jank causes that apply to janky scroll frames. -Each frame can have zero or multiple tags. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame ID. | -| tag | STRING | Tag of the scroll jank cause. | - -**chrome_tagged_janky_scroll_frames** - -TABLE -Consolidated list of tags for each janky scroll frame. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| tagged | BOOL | Whether this frame has any tags or not. | -| tags | STRING | Comma-separated list of tags for this frame. | - -### chrome.scroll_jank_v4 - -#### Views/Tables - -**chrome_scroll_jank_v4_results** - -TABLE -Results of the Scroll Jank V4 metric for frames which contain one or more -scroll updates. - -See - -and `EventLatency.ScrollJankV4Result` in - -for more information. - -Available since Chrome 145.0.7573.0 and cherry-picked into 144.0.7559.31. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Slice ID of the 'ScrollJankV4' slice. | -| name | STRING | Slice name ('ScrollJankV4'). | -| ts | TIMESTAMP | The timestamp at the start of the slice. | -| dur | DURATION | The duration of the slice. | -| is_janky | BOOL | Whether this frame is janky. True if and only if there's at least one row with `id` in `chrome_scroll_jank_v4_reasons`. If true, then `vsyncs_since_previous_frame` must be greater than one. | -| vsyncs_since_previous_frame | LONG | How many VSyncs were between (A) this frame and (B) the previous frame. If this value is greater than one, then Chrome potentially missed one or more VSyncs (i.e. might have been able to present this scroll update earlier). NULL if this frame is the first frame in a scroll. | -| running_delivery_cutoff | DURATION | The running delivery cut-off based on frames preceding this frame. NULL if ANY of the following holds: \* This frame is the first frame in a scroll. \* All frames since the beginning of the scroll up to and including the previous frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the previous frame have been non-damaging. | -| adjusted_delivery_cutoff | DURATION | The running delivery cut-off adjusted for this frame. NULL if ANY of the following holds: \* This frame is the first frame in a scroll. \* This frame is non-damaging. \* All frames since the beginning of the scroll up to and including the previous frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the previous frame have been non-damaging. \* `vsyncs_since_previous_frame` is equal to one. | -| current_delivery_cutoff | DURATION | The delivery cut-off of this frame. NULL if this frame is non-damaging. | -| real_first_event_latency_id | LONG | Trace ID of the first real scroll update included in this frame. Can be joined with `chrome_event_latencies.scroll_update_id`. NULL if this frame contains no real scroll updates. | -| real_first_input_generation_ts | TIMESTAMP | The actual generation timestamp of the first real scroll update included (coalesced) in this frame. NULL if this frame contains no real scroll updates. | -| real_last_input_generation_ts | TIMESTAMP | The actual generation timestamp of the last real scroll update included (coalesced) in this frame. NULL if this frame contains no real scroll updates. | -| real_abs_total_raw_delta_pixels | DOUBLE | The absolute total raw (unpredicted) delta of all real scroll updates included in this frame (in pixels). NULL if this frame contains no real scroll updates. | -| real_max_abs_inertial_raw_delta_pixels | DOUBLE | The maximum absolute raw (unpredicted) delta out of all inertial (fling) scroll updates included in this frame (in pixels). NULL if there were no inertial scroll updates in this frame. | -| synthetic_first_event_latency_id | LONG | Trace ID of the first synthetic scroll update included in this frame. Can be joined with `chrome_event_latencies.scroll_update_id`. NULL if this frame contains no synthetic scroll updates. | -| synthetic_first_extrapolated_input_generation_ts | TIMESTAMP | The generation timestamp of the first synthetic scroll update included (coalesced) in this frame extrapolated based on the input generation → begin frame duration of the most recent real scroll update. NULL if ANY of the following holds: \* This frame contains no synthetic scroll updates. \* This frame is janky (i.e. `is_janky` is true). \* All frames since the beginning of the scroll up to and including the previous frame have contained only synthetic scroll updates. \* The most recent janky frame contained only synthetic scroll updates and all frames since then up to and including the previous frame have contained only synthetic scroll updates. | -| synthetic_first_original_begin_frame_ts | TIMESTAMP | The begin frame timestamp of the first synthetic scroll update included (coalesced) in this frame. NULL if this frame contains no synthetic scroll updates. If not NULL, it's less than or equal to `begin_frame_ts`. | -| first_scroll_update_type | STRING | Type of the first scroll update in this frame. Possible values: \* 'REAL' \* 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' \* 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' The first scroll update is decided as follows: \* For real scroll updates, we consider their actual input generation timestamp. \* For synthetic scroll updates, we extrapolate their input generation timestamp based on the input generation → begin frame duration of the most recent real scroll update UNLESS ANY of the following holds (in which case we DON'T extrapolate input generation timestamps for synthetic scroll updates in this frame): \* This frame is janky. \* All frames since the beginning of the scroll up to and including the previous frame have contained only synthetic scroll updates. \* The most recent janky frame contained only synthetic scroll updates and all frames since then up to and including the previous frame have contained only synthetic scroll updates. If, based on the above rules, the scroll update with the earliest input generation timestamp is a real scroll update, then this frame's type is 'REAL'. If the scroll update with the earliest input generation timestamp is a synthetic scroll update, then this frame's type is 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP'. If this frame contains only synthetic scroll updates but it wasn't possible to extrapolate their input generation timestamp (for any of the reasons listed above), then this frame's type is 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP'. | -| first_event_latency_id | LONG | Trace ID of the first scroll update included in this frame. \* If `first_scroll_update_type` is 'REAL', then `first_event_latency_id` is equal to `real_first_event_latency_id`. \* If `first_scroll_update_type` is 'SYNTHETIC_WITH_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP' or 'SYNTHETIC_WITHOUT_EXTRAPOLATED_INPUT_GENERATION_TIMESTAMP', then `first_event_latency_id` equal to `synthetic_first_event_latency_id`. Can be joined with `chrome_event_latencies.scroll_update_id`. | -| damage_type | STRING | Type of scroll damage in this frame. Possible values: \* 'DAMAGING' \* 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP' \* 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP' A frame F is non-damaging if the following conditions are BOTH true: 1. All scroll updates in F are non-damaging. A scroll update is non-damaging if it didn't cause a frame update and/or didn't change the scroll offset. 2. All frames between (both ends exclusive): a. the last frame presented by Chrome before F and b. F are non-damaging. If this frame is damaging, its type is 'DAMAGING'. If this frame is non-damaging and its presentation timestamp could be extrapolated based on the begin frame → presentation duration of the most recent damaging frame, its type is 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP'. If this frame is non-damaging and its presentation timestamp couldn't be extrapolated for ANY of the reasons below, its type is 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP': \* This frame is janky and non-damaging. \* All frames since the beginning of the scroll up to and including this frame have been non-damaging. \* The most recent janky frame was non-damaging and all frames since then up to and including the this frame have been non-damaging. Note: The `first_scroll_update_type` and `damage_type` columns are orthogonal. The former depends on whether the frame is synthetic (only contains synthetic scroll updates). The latter depends on whether the frame is damaging. For example: \* If a frame is synthetic and damaging, it will\[1\] have an extrapolated input generation timestamp. \* If a frame is real and non-damaging, it will\[1\] have an extrapolated presentation timestamp. \* If a frame is both synthetic and damaging, it will\[1\] have both timestamps extrapolated. \[1\] As long as there's Chrome past performance to extrapolate based on. | -| vsync_interval | DURATION | The VSync interval that this frame was produced for according to the BeginFrameArgs. | -| begin_frame_ts | TIMESTAMP | The begin frame timestamp, at which this frame started, according to the BeginFrameArgs. | -| presentation_ts | TIMESTAMP | The presentation timestamp of the frame. \* If `damage_type` is 'DAMAGING', then `presentation_ts` is the actual presentation timestamp. \* If `damage_type` is 'NON_DAMAGING_WITH_EXTRAPOLATED_PRESENTATION_TIMESTAMP', then `presentation_ts` is an extrapolated timestamp based on the begin frame → presentation duration of the most recent damaging frame. \* If `damage_type` is 'NON_DAMAGING_WITHOUT_EXTRAPOLATED_PRESENTATION_TIMESTAMP', then `presentation_ts` is NULL. | - -**chrome_scroll_jank_v4_reasons** - -TABLE -Reasons why the Scroll Jank V4 metric marked frames as janky. - -A frame might be janky for multiple reasons, so this table might contain -multiple rows with the same `id` and distinct `jank_reason`s. - -Available since Chrome 145.0.7573.0 and cherry-picked into 144.0.7559.31. - -| Column | Type | Description | -|---|---|---| -| id | JOINID(slice.id) | Slice ID of the 'ScrollJankV4' slice. Can be joined with `chrome_scroll_jank_v4_results.id`. | -| jank_reason | STRING | A reason why the frame is janky. Possible values: \* 'MISSED_VSYNC_DUE_TO_DECELERATING_INPUT_FRAME_DELIVERY': Chrome's input→frame delivery slowed down to the point that it missed one or more VSyncs. \* 'MISSED_VSYNC_DURING_FAST_SCROLL': Chrome missed one or more VSyncs in the middle of a fast regular scroll. \* 'MISSED_VSYNC_AT_START_OF_FLING': Chrome missed one or more VSyncs during the transition from a fast regular scroll to a fling. \* 'MISSED_VSYNC_DURING_FLING': Chrome missed one or more VSyncs in the middle of a fling. | -| missed_vsyncs | LONG | Number of VSyncs that that Chrome missed (for `jank_reason`) before presenting the first scroll update in the frame. Greater than zero. | - -### chrome.speedometer - -#### Views/Tables - -**chrome_speedometer_measure** - -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the measure slice | -| dur | DURATION | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | LONG | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -**chrome_speedometer_iteration** - -TABLE -Slice that covers one Speedometer iteration. -Depending on the Speedometer version these slices might need to be estimated -as older versions of Speedometer to not emit marks for this interval. The -metrics associated are the same ones Speedometer would output, but note we -use ns precision (Speedometer uses \~100us) so the actual values might differ -a bit. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the iteration | -| dur | DURATION | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | LONG | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -#### Functions - -**chrome_speedometer_score** - -Returns DOUBLE: Speedometer score - -**chrome_speedometer_renderer_main_utid** - -Returns LONG: Renderer main utid - -### chrome.speedometer_2_1 - -#### Views/Tables - -**chrome_speedometer_2_1_measure** - -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async -sync is the time between the start and sync-end marks, async is the time -between the sync-end and async-end marks. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the measure slice | -| dur | DURATION | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | LONG | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -**chrome_speedometer_2_1_iteration** - -TABLE -Slice that covers one Speedometer iteration. -This slice is actually estimated as a default Speedometer run will not emit -marks to cover this interval. The metrics associated are the same ones -Speedometer would output, but note we use ns precision (Speedometer uses -\~100us) so the actual values might differ a bit. Also note Speedometer -returns the values in ms these here and in ns. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the iteration | -| dur | DURATION | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | LONG | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -#### Functions - -**chrome_speedometer_2_1_score** - -Returns the Speedometer 2.1 score for all iterations in the trace -Returns DOUBLE: Speedometer 2.1 score - -**chrome_speedometer_2_1_renderer_main_utid** - -Returns the utid for the main thread that ran Speedometer 2.1 -Returns LONG: Renderer main utid - -### chrome.speedometer_3 - -#### Views/Tables - -**chrome_speedometer_3_measure** - -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the measure slice | -| dur | DURATION | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | LONG | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -**chrome_speedometer_3_iteration** - -TABLE -Slice that covers one Speedometer iteration. -The metrics associated are the same ones -Speedometer would output, but note we use ns precision (Speedometer uses -\~100us) so the actual values might differ a bit. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start timestamp of the iteration | -| dur | DURATION | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | LONG | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -#### Functions - -**chrome_speedometer_3_score** - -Returns DOUBLE: Speedometer 3 score - -**chrome_speedometer_3_renderer_main_utid** - -Returns LONG: Renderer main utid - -### chrome.startups - -#### Views/Tables - -**chrome_startups** - -TABLE -Chrome startups, including launch cause. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Unique ID | -| activity_id | LONG | Chrome Activity event id of the launch. | -| name | STRING | Name of the launch start event. | -| startup_begin_ts | TIMESTAMP | Timestamp that the startup occurred. | -| first_visible_content_ts | TIMESTAMP | Timestamp to the first visible content. | -| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | -| browser_upid | LONG | Process ID of the Browser where the startup occurred. | - -### chrome.tasks - -#### Views/Tables - -**chrome_java_views** - -VIEW -A list of slices corresponding to operations on interesting (non-generic) -Chrome Java views. The view is considered interested if it's not a system -(ContentFrameLayout) or generic library (CompositorViewHolder) views. - -TODO(altimin): Add "columns_from slice" annotation. -TODO(altimin): convert this to EXTEND_TABLE when it becomes available. - -| Column | Type | Description | -|---|---|---| -| filtered_name | STRING | Name of the view. | -| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | -| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | -| slice_id | LONG | Slice id. | - -**chrome_scheduler_tasks** - -VIEW -A list of tasks executed by Chrome scheduler. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice id. | -| type | STRING | Type. | -| name | STRING | Name of the task. | -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration. | -| utid | LONG | Utid of the thread this task run on. | -| thread_name | STRING | Name of the thread this task run on. | -| upid | LONG | Upid of the process of this task. | -| process_name | STRING | Name of the process of this task. | -| track_id | LONG | Same as slice.track_id. | -| category | STRING | Same as slice.category. | -| depth | LONG | Same as slice.depth. | -| parent_id | LONG | Same as slice.parent_id. | -| arg_set_id | LONG | Same as slice.arg_set_id. | -| thread_ts | TIMESTAMP | Same as slice.thread_ts. | -| thread_dur | DURATION | Same as slice.thread_dur. | -| posted_from | STRING | Source location where the PostTask was called. | - -**chrome_tasks** - -VIEW -A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / -IPCs / system callbacks) run by Chrome. For a given thread, the slices -corresponding to these tasks will not intersect. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id for the given task, also the id of the slice this task corresponds to. | -| name | STRING | Name for the given task. | -| task_type | STRING | Type of the task (e.g. "scheduler"). | -| thread_name | STRING | Thread name. | -| utid | LONG | Utid. | -| process_name | STRING | Process name. | -| upid | LONG | Upid. | -| ts | TIMESTAMP | Alias of | -| dur | DURATION | Alias of | -| track_id | LONG | Alias of | -| category | STRING | Alias of | -| arg_set_id | LONG | Alias of | -| thread_ts | TIMESTAMP | Alias of | -| thread_dur | DURATION | Alias of | -| full_name | STRING | STRING Legacy alias for | - -### chrome.vsync_intervals - -#### Views/Tables - -**chrome_vsync_intervals** - -TABLE -A simple table that checks the time between VSync (this can be used to -determine if we're refreshing at 90 FPS or 60 FPS). - -> [!NOTE] -> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. - -| Column | Type | Description | -|---|---|---| -| slice_id | LONG | Slice id of the vsync slice. | -| ts | TIMESTAMP | Timestamp of the vsync slice. | -| dur | DURATION | Duration of the vsync slice. | -| track_id | LONG | Track id of the vsync slice. | -| time_to_next_vsync | LONG | Duration until next vsync arrives. | - -#### Functions - -**chrome_calculate_avg_vsync_interval** - -Function: compute the average Vysnc interval of the -gesture (hopefully this would be either 60 FPS for the whole gesture or 90 -FPS but that isnt always the case) on the given time segment. -If the trace doesnt contain the VSync TraceEvent we just fall back on -assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which -corresponds to 16 ms or 60 FPS). -Returns DOUBLE: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. - -| Argument | Type | Description | -|---|---|---| -| begin_ts | TIMESTAMP | Interval start time. | -| end_ts | TIMESTAMP | Interval end time. | - -### chrome.web_content_interactions - -#### Views/Tables - -**chrome_web_content_interactions** - -TABLE -Chrome web content interactions (InteractionToFirstPaint), including -associated high-level metrics and properties. - -Multiple events may occur for the same interaction; each row in this table -represents the primary (longest) event for the interaction. - -Web content interactions are discrete, as opposed to sustained (e.g. -scrolling); and only occur with the web content itself, as opposed to other -parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, -keyboard input (typing), and drags. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Unique id for this interaction. | -| ts | TIMESTAMP | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | -| dur | DURATION | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | -| interaction_type | STRING | The interaction type. | -| total_duration_ms | LONG | The total duration of all events that occurred for the same interaction. | -| renderer_upid | LONG | The process id this event occurred on. | - -## Package: android - -### android.anrs - -#### Views/Tables - -**android_anrs** - -TABLE -List of all ANRs that occurred in the trace (one row per ANR). - -| Column | Type | Description | -|---|---|---| -| process_name | STRING | Name of the process that triggered the ANR. | -| pid | LONG | PID of the process that triggered the ANR. | -| upid | JOINID(process.id) | UPID of the process that triggered the ANR. | -| error_id | STRING | UUID of the ANR (generated on the platform). | -| ts | TIMESTAMP | Timestamp of the ANR. | -| subject | STRING | Subject line of the ANR. | -| intent | STRING | The intent that caused the ANR (if applicable). | -| component | STRING | The component associated with the ANR (if applicable). | -| timer_delay | LONG | The duration between the timer expiration event and the anr counter event | -| anr_type | STRING | The standard type of ANR. | -| anr_dur_ms | LONG | Duration of the ANR, computed from the timer expiration event. | -| default_anr_dur_ms | LONG | Default duration of the ANR, based on the anr_type (default means in AOSP/Pixel). Note: Other OEMs may have customized these timeout values, so the defaults provided here might not be accurate for all devices. | - -### android.app_process_starts - -#### Views/Tables - -**android_app_process_starts** - -TABLE -All app cold starts with information about their cold start reason: -broadcast, service, activity or provider. - -| Column | Type | Description | -|---|---|---| -| start_id | LONG | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | -| id | LONG | Slice id of intent received in the app. | -| track_id | JOINID(track.id) | Track id of the intent received in the app. | -| process_name | STRING | Name of the process receiving the intent. | -| pid | LONG | Pid of the process receiving the intent. | -| upid | JOINID(process.id) | Upid of the process receiving the intent. | -| intent | STRING | Intent action or component responsible for the cold start. | -| reason | STRING | Process start reason: activity, broadcast, service or provider. | -| proc_start_ts | TIMESTAMP | Timestamp the process start was dispatched from system_server. | -| proc_start_dur | DURATION | Duration to dispatch the process start from system_server. | -| bind_app_ts | TIMESTAMP | Timestamp the bindApplication started in the app. | -| bind_app_dur | DURATION | Duration to complete bindApplication in the app. | -| intent_ts | TIMESTAMP | Timestamp the Intent was received in the app. | -| intent_dur | DURATION | Duration to handle intent in the app. | -| total_dur | LONG | Total duration from proc_start dispatched to intent completed. | - -### android.auto.multiuser - -#### Views/Tables - -**android_auto_multiuser_timing** - -TABLE -Time elapsed between the latest user start -and the specific end event -like package startup(ex carlauncher) or previous user stop. - -| Column | Type | Description | -|---|---|---| -| event_start_user_id | STRING | Id of the started android user | -| event_start_time | LONG | Start event time | -| event_end_time | LONG | End event time | -| event_end_name | STRING | End event name | -| event_start_name | STRING | Start event name | -| duration | LONG | User switch duration from start event to end event | - -**android_auto_multiuser_timing_with_previous_user_resource_usage** - -VIEW -This table extends `android_auto_multiuser_timing` table with previous user resource usage. - -| Column | Type | Description | -|---|---|---| -| event_start_user_id | STRING | Start user id | -| event_start_time | LONG | Start event time | -| event_end_time | LONG | End event time | -| event_end_name | STRING | End event name | -| event_start_name | STRING | Start event name | -| duration | LONG | User switch duration from start event to end event | -| user_id | LONG | User id | -| total_cpu_time | LONG | Total CPU time for a user | -| total_memory_usage_kb | LONG | Total memory user for a user | - -### android.battery - -#### Views/Tables - -**android_battery_charge** - -VIEW -Battery charge at timestamp. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | -| current_avg_ua | DOUBLE | Current average micro ampers. | -| capacity_percent | DOUBLE | Current capacity percentage. | -| charge_uah | DOUBLE | Current charge in micro ampers. | -| current_ua | DOUBLE | Current micro ampers. | -| voltage_uv | DOUBLE | Current voltage in micro volts. | -| energy_counter_uwh | DOUBLE | Current energy counter in microwatt-hours(µWh). | -| power_mw | DOUBLE | Current power in milliwatts. | - -### android.battery.charging_states - -#### Views/Tables - -**android_charging_states** - -TABLE -Device charging states. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Alias of counter.id if a slice with charging state exists otherwise there will be a single row where id = 1. | -| ts | TIMESTAMP | Timestamp at which the device charging state began. | -| dur | DURATION | Duration of the device charging state. | -| short_charging_state | STRING | One of: charging, discharging, not_charging, full, unknown. | -| charging_state | STRING | Device charging state, one of: Charging, Discharging, Not charging (when the charger is present but battery is not charging), Full, Unknown | - -### android.battery.doze - -#### Views/Tables - -**android_light_idle_state** - -TABLE -Light idle states. This is the state machine that quickly detects the -device is unused and restricts background activity. -See - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID | -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration. | -| light_idle_state | STRING | Description of the light idle state. | - -**android_deep_idle_state** - -TABLE -Deep idle states. This is the state machine that more slowly detects deeper -levels of device unuse and restricts background activity further. -See - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID | -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration. | -| deep_idle_state | STRING | Description of the deep idle state. | - -### android.battery_stats - -#### Views/Tables - -**android_battery_stats_state** - -VIEW -View of human readable battery stats counter-based states. These are recorded -by BatteryStats as a bitmap where each 'category' has a unique value at any -given time. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the new barrary state. | -| dur | DURATION | The duration the state was active, -1 for incomplete slices. | -| safe_dur | DURATION | The same as `dur`, but extends to trace end for incomplete slices. | -| track_name | STRING | The name of the counter track. | -| value | LONG | The counter value as a number. | -| value_name | STRING | The counter value as a human-readable string. | - -**android_battery_stats_event_slices** - -VIEW -View of slices derived from battery_stats events. Battery stats records all -events as instants, however some may indicate whether something started or -stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground -apps or long wakes include these details and allow drawing slices between -instant events found in a trace. - -For example, we may see an event like the following on 'battery_stats.top': - -`-top=10215:"com.google.android.apps.nexuslauncher"` This view will find the associated start ('+top') with the matching suffix -(everything after the '=') to construct a slice. It computes the timestamp -and duration from the events and extract the details as follows: - -`track_name='battery_stats.top' -str_value='com.google.android.apps.nexuslauncher' -int_value=10215` - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of a new battery state. | -| dur | DURATION | The duration the state was active, -1 for incomplete slices. | -| safe_dur | DURATION | The same as `dur`, but extends to trace end for incomplete slices. | -| track_name | STRING | The name of the counter track. | -| str_value | STRING | String value. | -| int_value | LONG | Int value. | - -#### Functions - -**android_battery_stats_counter_to_string** - -Converts a battery_stats counter value to human readable string. -Returns STRING: The human-readable name for the counter value. - -| Argument | Type | Description | -|---|---|---| -| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | -| value | DOUBLE | The counter value. | - -### android.binder - -#### Views/Tables - -**android_binder_metrics_by_process** - -VIEW -Count Binder transactions per process. - -| Column | Type | Description | -|---|---|---| -| process_name | STRING | Name of the process that started the binder transaction. | -| pid | LONG | PID of the process that started the binder transaction. | -| slice_name | STRING | Name of the slice with binder transaction. | -| event_count | LONG | Number of binder transactions in process in slice. | - -**android_sync_binder_thread_state_by_txn** - -VIEW -Aggregated thread_states on the client and server side per binder txn -This builds on the data from \|_sync_binder_metrics_by_txn\| and -for each end (client and server) of the transaction, it returns -the aggregated sum of all the thread state durations. -The \|thread_state_type\| column represents whether a given 'aggregated thread_state' -row is on the client or server side. 'binder_txn' is client side and 'binder_reply' -is server side. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | LONG | slice id of the binder txn | -| client_ts | TIMESTAMP | Client timestamp | -| client_tid | LONG | Client tid | -| binder_reply_id | LONG | slice id of the binder reply | -| server_ts | TIMESTAMP | Server timestamp | -| server_tid | LONG | Server tid | -| thread_state_type | STRING | whether thread state is on the txn or reply side | -| thread_state | STRING | a thread_state that occurred in the txn | -| thread_state_dur | DURATION | aggregated dur of the | -| thread_state_count | LONG | aggregated count of the | - -**android_sync_binder_blocked_functions_by_txn** - -VIEW -Aggregated blocked_functions on the client and server side per binder txn -This builds on the data from \|_sync_binder_metrics_by_txn\| and -for each end (client and server) of the transaction, it returns -the aggregated sum of all the kernel blocked function durations. -The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' -row is on the client or server side. 'binder_txn' is client side and 'binder_reply' -is server side. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | LONG | slice id of the binder txn | -| client_ts | TIMESTAMP | Client ts | -| client_tid | LONG | Client tid | -| binder_reply_id | LONG | slice id of the binder reply | -| server_ts | TIMESTAMP | Server ts | -| server_tid | LONG | Server tid | -| thread_state_type | STRING | whether thread state is on the txn or reply side | -| blocked_function | STRING | blocked kernel function in a thread state | -| blocked_function_dur | DURATION | aggregated dur of the | -| blocked_function_count | LONG | aggregated count of the | - -**android_binder_txns** - -TABLE -Breakdown binder transactions per txn. -It returns data about the client and server ends of every binder transaction async. - -| Column | Type | Description | -|---|---|---| -| aidl_name | STRING | Fully qualified name of the binder endpoint if existing. | -| interface | STRING | Interface of the binder endpoint if existing. | -| method_name | STRING | Method name of the binder endpoint if existing. | -| aidl_ts | TIMESTAMP | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | -| aidl_dur | DURATION | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | -| binder_txn_id | JOINID(slice.id) | Slice id of the binder txn. | -| client_process | STRING | Name of the client process. | -| client_thread | STRING | Name of the client thread. | -| client_upid | JOINID(process.id) | Upid of the client process. | -| client_utid | JOINID(thread.id) | Utid of the client thread. | -| client_tid | LONG | Tid of the client thread. | -| client_pid | LONG | Pid of the client thread. | -| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | -| client_ts | TIMESTAMP | Timestamp of the client txn. | -| client_dur | DURATION | Wall clock dur of the client txn. | -| binder_reply_id | JOINID(slice.id) | Slice id of the binder reply. | -| server_process | STRING | Name of the server process. | -| server_thread | STRING | Name of the server thread. | -| server_upid | JOINID(process.id) | Upid of the server process. | -| server_utid | JOINID(thread.id) | Utid of the server thread. | -| server_tid | LONG | Tid of the server thread. | -| server_pid | LONG | Pid of the server thread. | -| server_ts | TIMESTAMP | Timestamp of the server txn. | -| server_dur | DURATION | Wall clock dur of the server txn. | -| client_oom_score | LONG | Oom score of the client process at the start of the txn. | -| server_oom_score | LONG | Oom score of the server process at the start of the reply. | -| is_sync | BOOL | Whether the txn is synchronous or async (oneway). | -| client_monotonic_dur | DURATION | Monotonic clock dur of the client txn. | -| server_monotonic_dur | DURATION | Monotonic clock dur of the server txn. | -| client_package_version_code | LONG | Client package version_code. | -| server_package_version_code | LONG | Server package version_code. | -| is_client_package_debuggable | BOOL | Whether client package is debuggable. | -| is_server_package_debuggable | BOOL | Whether server package is debuggable. | - -#### Table Functions - -**android_binder_outgoing_graph** - -Returns a DAG of all outgoing binder txns from a process. -The roots of the graph are the threads making the txns and the graph flows from: -thread -\> server_process -\> AIDL interface -\> AIDL method. -The weights of each node represent the wall execution time in the server_process. - -| Argument | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Upid of process to generate an outgoing graph for. | - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of outgoing binder txns. | - -**android_binder_incoming_graph** - -Returns a DAG of all incoming binder txns from a process. -The roots of the graph are the clients making the txns and the graph flows from: -client_process -\> AIDL interface -\> AIDL method. -The weights of each node represent the wall execution time in the server_process. - -| Argument | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Upid of process to generate an incoming graph for. | - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of incoming binder txns. | - -**android_binder_graph** - -Returns a graph of all binder txns in a trace. -The nodes are client_process and server_process. -The weights of each node represent the wall execution time in the server_process. - -| Argument | Type | Description | -|---|---|---| -| min_client_oom_score | LONG | Matches txns from client_processes greater than or equal to the OOM score. | -| max_client_oom_score | LONG | Matches txns from client_processes less than or equal to the OOM score. | -| min_server_oom_score | LONG | Matches txns to server_processes greater than or equal to the OOM score. | -| max_server_oom_score | LONG | Matches txns to server_processes less than or equal to the OOM score. | - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of binder txns. | - -### android.binder_breakdown - -#### Views/Tables - -**android_binder_server_breakdown** - -TABLE -Server side binder breakdowns per transactions per txn. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | -| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | -| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder reply with a single reason. | -| dur | DURATION | Duration of an exclusive interval during the binder reply with a single reason. | -| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | - -**android_binder_client_breakdown** - -TABLE -Client side binder breakdowns per transactions per txn. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | -| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | -| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder txn with a single latency reason. | -| dur | DURATION | Duration of an exclusive interval during the binder txn with a single latency reason. | -| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | - -**android_binder_client_server_breakdown** - -TABLE -Combined client and server side binder breakdowns per transaction. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | JOINID(slice.id) | Client side id of the binder txn. | -| binder_reply_id | JOINID(slice.id) | Server side id of the binder txn. | -| ts | TIMESTAMP | Timestamp of an exclusive interval during the binder txn with a single latency reason. | -| dur | DURATION | Duration of an exclusive interval during the binder txn with a single latency reason. | -| server_reason | STRING | The server side component of this interval's binder latency reason, if any. | -| client_reason | STRING | The client side component of this interval's binder latency reason. | -| reason | STRING | Combined reason indicating whether latency came from client or server side. | -| reason_type | STRING | Whether the latency is due to the client or server. | - -### android.bitmaps - -#### Views/Tables - -**android_bitmap_memory** - -TABLE -Provides a timeseries of "Bitmap Memory" counter for each process, which -is useful for retrieving the total memory used by bitmaps by an application over time. - -To populate this table, tracing must be enabled with the "view" atrace -category. - -| Column | Type | Description | -|---|---|---| -| id | ID(counter.id) | ID of the row in the underlying counter table. | -| upid | JOINID(process.upid) | Upid of the process. | -| ts | TIMESTAMP | Timestamp of the start of the interval. | -| dur | DURATION | Duration of the interval. | -| track_id | JOINID(counter.track_id) | Duration of the interval. | -| value | LONG | Memory consumed by bitmaps in bytes. | - -**android_bitmap_count** - -TABLE -Provides a timeseries of "Bitmap Count" counter for each process, which -is useful for retrieving the number of bitmaps used by an application over time. - -To populate this table, tracing must be enabled with the "view" atrace -category. - -| Column | Type | Description | -|---|---|---| -| id | ID(counter.id) | ID of the row in the underlying counter table. | -| upid | JOINID(process.upid) | Upid of the process. | -| ts | TIMESTAMP | Timestamp of the start of the interval. | -| dur | DURATION | Duration of the interval. | -| track_id | JOINID(counter.track_id) | Duration of the interval. | -| value | LONG | Number of allocated bitmaps. | - -**android_bitmap_counters_per_process** - -TABLE -Provides a timeseries of bitmap-related counters for each process, which -is useful for understanding an application's bitmap usage over time. - -To populate this table, tracing must be enabled with the "view" atrace -category. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.upid) | Upid of the process. | -| process_name | STRING | Name of the process. | -| ts | TIMESTAMP | Timestamp of the start of the interval. | -| dur | DURATION | Duration of the interval. | -| bitmap_memory | LONG | Memory consumed by bitmaps in bytes. | -| bitmap_count | LONG | Number of allocated bitmaps. | -| bitmap_memory_id | JOINID(counter.id) | ID of the row in the underlying counter table. | -| bitmap_count_id | JOINID(counter.id) | ID of the row in the underlying counter table. | - -### android.cpu.cluster_type - -#### Views/Tables - -**android_cpu_cluster_mapping** - -TABLE -Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. -This cluster type is determined by initially using cpu_capacity from sysfs -and grouping clusters with identical capacities, ordered by size. -In the case that capacities are not present, max frequency is used instead. -If nothing is avaiable, NULL is returned. - -| Column | Type | Description | -|---|---|---| -| ucpu | LONG | Alias of `cpu.ucpu`. | -| cpu | LONG | Alias of `cpu.cpu`. | -| cluster_type | STRING | The cluster type of the CPU. | - -### android.cpu.cpu_per_uid - -#### Views/Tables - -**android_cpu_per_uid_track** - -TABLE -Table of tracks for CPU-per-UID data. Each row represents one UID / cluster -combination. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the track; can be joined with cpu_per_uid_counter. | -| uid | LONG | UID doing the work. | -| cluster | LONG | Cluster ID for the track, starting from 0, typically with larger numbers meaning larger cores. | -| total_cpu_millis | LONG | Total number of cpu millis used by this track. | -| package_name | STRING | A package name for the UID. If there are multiple for a UID, one is chosen arbitrarily. UIDs below 10000 always have null package name. | - -**android_cpu_per_uid_counter** - -VIEW -View of counters for CPU-per-UID data. Each row represents one instant in -time for one UID / cluster. - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID for the row. | -| ts | LONG | Timestamp for the row. | -| dur | LONG | Time to the next measurement for the UID / cluster combination. | -| track_id | LONG | Associated track. | -| diff_ms | LONG | CPU time measurement for this time period (milliseconds). | -| cpu_ratio | DOUBLE | Inferred CPU use value for the period where 1.0 means a single core at 100% utilisation. | - -### android.cujs.base - -#### Views/Tables - -**android_jank_cuj** - -TABLE -Table tracking all jank CUJs information. - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | -| cuj_slice_name | STRING | Name of the CUJ slice. | -| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | -| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | -| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | -| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | -| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | -| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | -| ui_thread | JOINID(thread.id) | thread id of the UI thread. | -| layer_id | LONG | layer id associated with the actual frame. | -| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | -| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | - -### android.cujs.sysui_cujs - -#### Views/Tables - -**android_sysui_jank_cujs** - -TABLE -Table tracking all jank CUJs information. - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | -| cuj_slice_name | STRING | Name of the CUJ slice. | -| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | -| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | -| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | -| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | -| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | -| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | -| ui_thread | JOINID(thread.id) | thread id of the UI thread. | -| layer_id | LONG | layer id associated with the actual frame. | -| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | -| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | - -**android_sysui_latency_cujs** - -TABLE -Table tracking all latency CUJs information. - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | -| cuj_slice_name | STRING | Name of the CUJ slice. | -| cuj_name | STRING | Name of the CUJ without the 'L\<' prefix. | -| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | -| ts | TIMESTAMP | Start timestamp of the CUJ calculated as the start of the CUJ slice in trace. | -| ts_end | TIMESTAMP | End timestamp of the CUJ calculated as the end timestamp of the CUJ slice. | -| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | -| state | STRING | State of the CUJ whether it was completed/cancelled. | - -**android_jank_latency_cujs** - -TABLE -Table tracking all jank/latency CUJs information. - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| id | LONG | An alias for cuj_id for compatibility purposes. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | -| cuj_slice_name | STRING | Name of the CUJ slice. | -| cuj_name | STRING | Name of the CUJ without the 'J\<' prefix. | -| slice_id | JOINID(slice.id) | Id of the CUJ slice in perfetto. Keeping the slice id column as part of this table as provision to lookup the actual CUJ slice ts and dur. The ts and dur in this table might differ from the slice duration, as they are associated with start and end frame corresponding to the CUJ. | -| ts | TIMESTAMP | Start timestamp of the CUJ. Start of the CUJ as defined by the start of the first overlapping expected frame. | -| ts_end | TIMESTAMP | End timestamp of the CUJ. Calculated as the end timestamp of the last actual frame overlapping with the CUJ. | -| dur | DURATION | Duration of the CUJ calculated based on the ts and ts_end values. | -| state | STRING | State of the CUJ. One of "completed", "cancelled" or NULL. NULL in cases where the FT#cancel or FT#end instant event is not present for the CUJ. | -| ui_thread | JOINID(thread.id) | thread id of the UI thread. In case of latency CUJs, this will always be the main thread of the process. | -| layer_id | LONG | layer id associated with the actual frame. | -| begin_vsync | LONG | vysnc id of the first frame that falls within the CUJ boundary. | -| end_vsync | LONG | vysnc id of the last frame that falls within the CUJ boundary. | -| cuj_type | STRING | Type of CUJ, i.e. jank or latency. | - -### android.cujs.threads - -#### Views/Tables - -**android_jank_cuj_render_thread** - -TABLE -Table captures thread information for 'RenderThread' for all CUJs. - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| upid | JOINID(process.id) | process id. | -| utid | JOINID(thread.id) | thread id of the main/UI thread. | -| name | STRING | thread name. | -| track_id | JOINID(track.id) | track_id for the thread. | - -#### Table Functions - -**android_jank_cuj_app_thread** - -Returns a table with all CUJs and an additional column for the track id of thread_name -passed as parameter, if present in the same process of the cuj. - -| Argument | Type | Description | -|---|---|---| -| thread_name | STRING | Name of the thread for which information needs to be extracted. | - -| Column | Type | Description | -|---|---|---| -| cuj_id | LONG | Unique incremental ID for each CUJ. | -| upid | JOINID(process.id) | process id. | -| utid | LONG | thread id of the input thread. | -| name | STRING | name of the thread. | -| track_id | LONG | track id associated with the thread. | - -### android.desktop_mode - -#### Views/Tables - -**android_desktop_mode_windows** - -TABLE -Desktop Windows with durations they were open. - -| Column | Type | Description | -|---|---|---| -| raw_add_ts | TIMESTAMP | Window add timestamp; NULL if no add event in the trace. | -| raw_remove_ts | TIMESTAMP | Window remove timestamp; NULL if no remove event in the trace. | -| ts | TIMESTAMP | Timestamp that the window was added; or trace_start() if no add event in the trace. | -| dur | DURATION | Furation the window was open; or until trace_end() if no remove event in the trace. | -| instance_id | LONG | Desktop Window instance ID - unique per window. | -| uid | LONG | UID of the app running in the window. | - -### android.device - -#### Views/Tables - -**android_device_name** - -TABLE -Extract name of the device based on metadata from the trace. - -| Column | Type | Description | -|---|---|---| -| name | STRING | Device name. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | - -### android.dumpsys.show_map - -#### Views/Tables - -**android_dumpsys_show_map** - -TABLE -This table represents memory mapping information from /proc/\[pid\]/smaps -All memory values are in kilobytes (KB) - -| Column | Type | Description | -|---|---|---| -| process_name | STRING | Name of the process. | -| pid | JOINID(process.pid) | Process ID. | -| vss_kb | LONG | Virtual Set Size in kilobytes - total virtual memory mapped by the process. | -| rss_kb | LONG | Resident Set Size in kilobytes - actual physical memory used by the process. | -| pss_kb | LONG | Proportional Set Size in kilobytes - amount of memory shared with other processes. | -| shared_clean_kb | LONG | Clean shared pages in kilobytes - shared pages that haven't been modified. | -| shared_dirty_kb | LONG | Dirty shared pages in kilobytes - shared pages that have been modified. | -| private_clean_kb | LONG | Clean private pages in kilobytes - private pages that haven't been modified. | -| private_dirty_kb | LONG | Dirty private pages in kilobytes - private pages that have been modified. | -| swap_kb | LONG | Swap memory in kilobytes - memory that has been moved to swap space. | -| swap_pss_kb | LONG | Proportional Swap Size in kilobytes - swap shared with other processes. | -| anon_huge_pages_kb | LONG | Anonymous huge pages in kilobytes - large anonymous memory regions. | -| shmem_pmd_mapped_kb | LONG | Shared Memory PMD mapped in kilobytes - page middle directory mapped shared memory. | -| file_pmd_mapped_kb | LONG | File PMD mapped in kilobytes - page middle directory mapped file memory. | -| shared_huge_tlb_kb | LONG | Shared huge TLB in kilobytes - shared huge page table entries. | -| private_hugetlb_kb | LONG | Private huge TLB in kilobytes - private huge page table entries. | -| locked_kb | LONG | Locked memory in kilobytes - memory that can't be swapped out. | -| mapping_count | LONG | Number of mappings of the object. | -| mapped_object | STRING | Path to the mapped object (file, library, etc.). | - -### android.dvfs - -#### Views/Tables - -**android_dvfs_counters** - -VIEW -Dvfs counter with duration. - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name. | -| ts | TIMESTAMP | Timestamp when counter value changed. | -| value | DOUBLE | Counter value. | -| dur | DURATION | Counter duration. | - -**android_dvfs_counter_stats** - -TABLE -Aggregates dvfs counter slice for statistic. - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name on which all the other values are aggregated on. | -| max | DOUBLE | Max of all counter values for the counter name. | -| min | DOUBLE | Min of all counter values for the counter name. | -| dur | DURATION | Duration between the first and last counter value for the counter name. | -| wgt_avg | DOUBLE | Weighted avergate of all the counter values for the counter name. | - -**android_dvfs_counter_residency** - -VIEW - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name. | -| value | DOUBLE | Counter value. | -| dur | DURATION | Counter duration. | -| pct | DOUBLE | Counter duration as a percentage of total duration. | - -### android.entity_state_residency - -#### Views/Tables - -**android_entity_state_residency** - -TABLE -Android entity state residency samples. -For details see: - -| Column | Type | Description | -|---|---|---| -| id | ID(counter.id) | `counter.id` | -| ts | TIMESTAMP | Timestamp of the residency sample. | -| dur | DURATION | Time until the next residency sample. | -| entity_name | STRING | Entity or subsytem name. | -| state_name | STRING | State name | -| raw_name | STRING | Raw name (alias of counter.name) | -| state_time_since_boot | DURATION | Time the entity or subsystem spent in the state since boot | -| state_time_since_boot_at_end | DURATION | Time the entity or subsystem spent in the state since boot on the next sample | -| state_time_ratio | DOUBLE | ratio of the time the entity or subsystem spend in the state out of the elapsed time of the sample period. A value of 1 typically means the 100% of time was spent in the state, and a value of 0 means no time was spent. | -| track_id | JOINID(track.id) | entity + state track id. Alias of `counter_track.id`. | - -### android.frames.jank_type - -#### Functions - -**android_is_sf_jank_type** - -Returns BOOL: True when the jank type represents sf jank - -| Argument | Type | Description | -|---|---|---| -| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | - -**android_is_app_jank_type** - -Returns BOOL: True when the jank type represents app jank - -| Argument | Type | Description | -|---|---|---| -| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | - -**android_is_missed_frame_type** - -Returns BOOL: True if jank_type represents missed frame jank - -| Argument | Type | Description | -|---|---|---| -| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | - -### android.frames.per_frame_metrics - -#### Views/Tables - -**android_frames_overrun** - -TABLE -The amount by which each frame missed of hit its deadline. Negative if the -deadline was not missed. Frames are considered janky if `overrun` is -positive. -Calculated as the difference between the end of the -`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the -frame. -Availability: from S (API 31). -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| overrun | LONG | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | - -**android_frames_ui_time** - -TABLE -How much time did the frame's Choreographer callbacks take. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id | -| ui_time | LONG | UI time duration | - -**android_app_vsync_delay_per_frame** - -TABLE -App Vsync delay for a frame. The time between the VSYNC-app signal and the -start of Choreographer work. -Calculated as time difference between the actual frame start (from -`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` -slice. -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id | -| app_vsync_delay | LONG | App VSYNC delay. | - -**android_cpu_time_per_frame** - -TABLE -How much time did the frame take across the UI Thread + RenderThread. -Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice -duration and summed durations of all `DrawFrame` slices associated with this -frame. -Availability: from N (API 24). -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id | -| app_vsync_delay | LONG | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | -| do_frame_dur | DURATION | Duration of `Choreographer#doFrame` slice. | -| draw_frame_dur | DURATION | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | -| cpu_time | LONG | CPU time across the UI Thread + RenderThread. | - -**android_frame_stats** - -TABLE -Aggregated stats of the frame. - -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| overrun | LONG | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | -| cpu_time | LONG | How much time did the frame take across the UI Thread + RenderThread. | -| ui_time | LONG | How much time did the frame's Choreographer callbacks take. | -| was_jank | BOOL | Was frame janky. | -| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | -| was_big_jank | BOOL | CPU time of the frame took over 50ms. | -| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | - -### android.frames.timeline - -#### Views/Tables - -**android_frames_choreographer_do_frame** - -TABLE -All of the `Choreographer#doFrame` slices with their frame id. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Choreographer#doFrame slice. Slice with the name "Choreographer#doFrame {frame id}". | -| frame_id | LONG | Frame id. Taken as the value behind "Choreographer#doFrame" in slice name. | -| ui_thread_utid | JOINID(thread.id) | Utid of the UI thread | -| upid | JOINID(process.id) | Upid of application process | -| ts | TIMESTAMP | Timestamp of the slice. | - -**android_frames_draw_frame** - -TABLE -All of the `DrawFrame` slices with their frame id and render thread. -There might be multiple DrawFrames slices for a single vsync (frame id). -This happens when we are drawing multiple layers (e.g. status bar and -notifications). - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | DrawFrame slice. Slice with the name "DrawFrame {frame id}". | -| frame_id | LONG | Frame id. Taken as the value behind "DrawFrame" in slice name. | -| render_thread_utid | JOINID(thread.id) | Utid of the render thread | -| upid | JOINID(process.id) | Upid of application process | - -**android_frames_layers** - -TABLE -TODO(b/384322064) Match actual timeline slice with correct draw frame using layer name. -All slices related to one frame. Aggregates `Choreographer#doFrame`, -`actual_frame_timeline_slice` and `expected_frame_timeline_slice` slices. -This table differs slightly from the android_frames table, as it -captures the layer_id for each actual timeline slice too. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| ts | TIMESTAMP | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | -| dur | DURATION | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | -| ts_end | TIMESTAMP | End timestamp of the frame. End of the frame as defined by the sum of start timestamp and duration of the frame. | -| do_frame_id | JOINID(slice.id) | `slice.id` of "Choreographer#doFrame" slice. | -| draw_frame_id | JOINID(slice.id) | `slice.id` of "DrawFrame" slice. For now, we only support the first DrawFrame slice (due to b/384322064). | -| actual_frame_timeline_id | JOINID(slice.id) | `slice.id` from `actual_frame_timeline_slice` | -| expected_frame_timeline_id | JOINID(slice.id) | `slice.id` from `expected_frame_timeline_slice` | -| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | -| ui_thread_utid | JOINID(thread.id) | thread id of the UI thread. | -| layer_id | LONG | layer id associated with the actual frame. | -| layer_name | STRING | layer name associated with the actual frame. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | - -**android_frames** - -TABLE -Table based on the android_frames_layers table. It aggregates time, duration and counts -information across different layers for a given frame_id in a given process. - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| ts | TIMESTAMP | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | -| dur | DURATION | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | -| do_frame_id | JOINID(slice.id) | `slice.id` of "Choreographer#doFrame" slice. | -| draw_frame_id | JOINID(slice.id) | `slice.id` of "DrawFrame" slice. For now, we only support the first DrawFrame slice (due to b/384322064). | -| actual_frame_timeline_id | JOINID(slice.id) | `slice.id` from `actual_frame_timeline_slice` | -| expected_frame_timeline_id | JOINID(slice.id) | `slice.id` from `expected_frame_timeline_slice` | -| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | -| ui_thread_utid | JOINID(thread.id) | thread id of the UI thread. | -| actual_frame_timeline_count | LONG | Count of slices in `actual_frame_timeline_slice` related to this frame. | -| expected_frame_timeline_count | LONG | Count of slices in `expected_frame_timeline_slice` related to this frame. | -| draw_frame_count | LONG | Count of draw_frame associated to this frame. | -| upid | JOINID(process.id) | process id. | -| process_name | STRING | process name. | - -#### Table Functions - -**android_first_frame_after** - -Returns first frame after the provided timestamp. The returning table has at -most one row. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | - -| Column | Type | Description | -|---|---|---| -| frame_id | LONG | Frame id. | -| ts | TIMESTAMP | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | -| dur | DURATION | Duration of the frame. | -| do_frame_id | JOINID(slice.id) | "Choreographer#doFrame" slice. The slice with name "Choreographer#doFrame" corresponding to this frame. | -| draw_frame_id | JOINID(slice.id) | "DrawFrame" slice. The slice with name "DrawFrame" corresponding to this frame. | -| actual_frame_timeline_id | JOINID(slice.id) | actual_frame_timeline_slice\` slice related to this frame. | -| expected_frame_timeline_id | JOINID(slice.id) | `expected_frame_timeline_slice` slice related to this frame. | -| render_thread_utid | JOINID(thread.id) | `utid` of the render thread. | -| ui_thread_utid | JOINID(thread.id) | `utid` of the UI thread. | - -### android.freezer - -#### Views/Tables - -**android_freezer_events** - -TABLE -All frozen processes and their frozen duration. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Upid of frozen process | -| pid | LONG | Pid of frozen process | -| ts | TIMESTAMP | Timestamp process was frozen. | -| dur | DURATION | Duration process was frozen for. | -| unfreeze_reason_int | LONG | Unfreeze reason Integer. | -| unfreeze_reason_str | STRING | Unfreeze reason String. | - -### android.garbage_collection - -#### Views/Tables - -**android_garbage_collection_events** - -TABLE -All Garbage collection events with a breakdown of the time spent and heap reclaimed. - -| Column | Type | Description | -|---|---|---| -| tid | LONG | Tid of thread running garbage collection. | -| pid | LONG | Pid of process running garbage collection. | -| utid | JOINID(thread.id) | Utid of thread running garbage collection. | -| upid | JOINID(process.id) | Upid of process running garbage collection. | -| thread_name | STRING | Name of thread running garbage collection. | -| process_name | STRING | Name of process running garbage collection. | -| gc_type | STRING | Type of garbage collection. | -| is_mark_compact | LONG | Whether gargage collection is mark compact or copying. | -| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | -| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | -| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | -| gc_id | LONG | Garbage collection id. | -| gc_ts | TIMESTAMP | Garbage collection timestamp. | -| gc_dur | DURATION | Garbage collection wall duration. | -| gc_running_dur | DURATION | Garbage collection duration spent executing on CPU. | -| gc_runnable_dur | DURATION | Garbage collection duration spent waiting for CPU. | -| gc_unint_io_dur | DURATION | Garbage collection duration spent waiting in the Linux kernel on IO. | -| gc_unint_non_io_dur | DURATION | Garbage collection duration spent waiting in the Linux kernel without IO. | -| gc_int_dur | LONG | Garbage collection duration spent waiting in interruptible sleep. | - -### android.gpu.frequency - -#### Views/Tables - -**android_gpu_frequency** - -TABLE -GPU frequency counter per GPU. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| gpu_id | LONG | GPU id. Joinable with `gpu_counter_track.gpu_id`. | -| gpu_freq | LONG | GPU frequency | -| prev_gpu_freq | LONG | GPU frequency from previous slice | -| next_gpu_freq | LONG | GPU frequency from next slice | - -### android.gpu.mali_power_state - -#### Views/Tables - -**android_mali_gpu_power_state** - -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| power_state | LONG | GPU power state | - -### android.gpu.memory - -#### Views/Tables - -**android_gpu_memory_per_process** - -TABLE -Counter for GPU memory per process with duration. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| upid | JOINID(process.id) | Upid of the process | -| gpu_memory | LONG | GPU memory | - -### android.gpu.work_period - -#### Views/Tables - -**android_gpu_work_period_track** - -TABLE -Tracks for GPU work period events originating from the -`power/gpu_work_period` Linux ftrace tracepoint. - -This tracepoint is usually only available on selected Android devices. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Unique identifier for this track. Joinable with track.id. | -| machine_id | LONG | Machine identifier, non-null for tracks on a remote machine. | -| uid | LONG | The UID of the package for which the GPU work period events were emitted. | -| gpu_id | LONG | The GPU identifier for which the GPU work period events were emitted. | - -### android.input - -#### Views/Tables - -**android_input_events** - -TABLE -All input events with round trip latency breakdown. Input delivery is socket based and every -input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure -latencies between: - -1. Input dispatch event sent from OS. -2. Input dispatch event received in app. -3. Input ACK event sent from app. -4. Input ACK event received in OS. - -| Column | Type | Description | -|---|---|---| -| dispatch_latency_dur | DURATION | Duration from input dispatch to input received. | -| handling_latency_dur | DURATION | Duration from input received to input ACK sent. | -| ack_latency_dur | DURATION | Duration from input ACK sent to input ACK received. | -| total_latency_dur | DURATION | Duration from input dispatch to input event ACK received. | -| end_to_end_latency_dur | DURATION | Duration from input read to frame present time. Null if an input event has no associated frame event. | -| tid | LONG | Tid of thread receiving the input event. | -| thread_name | STRING | Name of thread receiving the input event. | -| pid | LONG | Pid of process receiving the input event. | -| process_name | STRING | Name of process receiving the input event. | -| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | -| event_action | STRING | Input event action. | -| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | -| event_channel | STRING | Input event channel name. | -| input_event_id | STRING | Unique identifier for the input event. | -| read_time | LONG | Timestamp input event was read by InputReader. | -| dispatch_track_id | JOINID(track.id) | Thread track id of input event dispatching thread. | -| dispatch_ts | TIMESTAMP | Timestamp input event was dispatched. | -| dispatch_dur | DURATION | Duration of input event dispatch. | -| receive_track_id | JOINID(track.id) | Thread track id of input event receiving thread. | -| receive_ts | TIMESTAMP | Timestamp input event was received. | -| receive_dur | DURATION | Duration of input event receipt. | -| frame_id | LONG | Vsync Id associated with the input. Null if an input event has no associated frame event. | - -**android_key_events** - -VIEW -Key events processed by the Android framework (from android.input.inputevent data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the trace entry | -| event_id | LONG | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | -| ts | TIMESTAMP | The timestamp of when the input event was processed by the system | -| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | -| source | LONG | Event source e.g. touchscreen, keyboard | -| action | LONG | Action e.g. down, move | -| device_id | LONG | Device id | -| display_id | LONG | Display id | -| key_code | LONG | Key code | - -**android_motion_events** - -VIEW -Motion events processed by the Android framework (from android.input.inputevent data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the trace entry | -| event_id | LONG | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | -| ts | TIMESTAMP | The timestamp of when the input event was processed by the system | -| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | -| source | LONG | Event source e.g. touchscreen, keyboard | -| action | LONG | Action e.g. down, move | -| device_id | LONG | Device id | -| display_id | LONG | Display id | - -**android_input_event_dispatch** - -VIEW -Input event dispatching information in Android (from android.input.inputevent data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | ID of the trace entry | -| event_id | LONG | Event ID of the input event that was dispatched | -| arg_set_id | ARGSETID | Details of the input event parsed from the proto message | -| vsync_id | LONG | Vsync ID that identifies the state of the windows during which the dispatch decision was made | -| window_id | LONG | Window ID of the window receiving the event | - -### android.job_scheduler - -#### Views/Tables - -**android_job_scheduler_events** - -TABLE -All scheduled jobs and their latencies. - -The table is populated by ATrace using the system server ATrace category -(`atrace_categories: "ss"`). You can also set the `atrace_apps` of interest. - -This differs from the `android_job_scheduler_states` table -in the `android.job_scheduler_states` module which is populated -by the `ScheduledJobStateChanged` atom. - -Using `android_job_scheduler_states` is preferred when the -`ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in the trace since -it includes the constraint, screen, or charging state changes for -each job in a trace. - -| Column | Type | Description | -|---|---|---| -| job_id | LONG | Id of the scheduled job assigned by the app developer. | -| uid | LONG | Uid of the process running the scheduled job. | -| package_name | STRING | Package name of the process running the scheduled job. | -| job_service_name | STRING | Service component name of the scheduled job. | -| track_id | JOINID(track.id) | Thread track id of the job scheduler event slice. | -| id | LONG | Slice id of the job scheduler event slice. | -| ts | TIMESTAMP | Timestamp the job was scheduled. | -| dur | DURATION | Duration of the scheduled job. | - -### android.job_scheduler_states - -#### Views/Tables - -**android_job_scheduler_states** - -TABLE -This table returns constraint changes that a -job will go through in a single trace. - -Values in this table are derived from the the `ScheduledJobStateChanged` -atom. This table differs from the -`android_job_scheduler_with_screen_charging_states` in this module -(`android.job_scheduler_states`) by only having job constraint information. - -See documentation for the `android_job_scheduler_with_screen_charging_states` -for how tables in this module differ from `android_job_scheduler_events` -table in the `android.job_scheduler` module and how to populate this table. - -| Column | Type | Description | -|---|---|---| -| id | ID | Unique identifier for job scheduler state. | -| ts | TIMESTAMP | Timestamp of job state slice. | -| dur | DURATION | Duration of job state slice. | -| slice_id | JOINID(slice.id) | Id of the slice. | -| job_name | STRING | Name of the job (as named by the app). | -| uid | LONG | Uid associated with job. | -| job_id | LONG | Id of job (assigned by app for T- builds and system generated in U+ builds). | -| package_name | STRING | Package that the job belongs (ex: associated app). | -| job_namespace | STRING | Namespace of job. | -| effective_priority | LONG | Priority at which JobScheduler ran the job. | -| has_battery_not_low_constraint | BOOL | True if app requested job should run when the device battery is not low. | -| has_charging_constraint | BOOL | True if app requested job should run when the device is charging. | -| has_connectivity_constraint | BOOL | True if app requested job should run when device has connectivity. | -| has_content_trigger_constraint | BOOL | True if app requested job should run when there is a content trigger. | -| has_deadline_constraint | BOOL | True if app requested there is a deadline by which the job should run. | -| has_idle_constraint | BOOL | True if app requested job should run when device is idle. | -| has_storage_not_low_constraint | BOOL | True if app requested job should run when device storage is not low. | -| has_timing_delay_constraint | BOOL | True if app requested job has a timing delay. | -| is_prefetch | BOOL | True if app requested job should run within hours of app launch. | -| is_requested_expedited_job | BOOL | True if app requested that the job is run as an expedited job. | -| is_running_as_expedited_job | BOOL | The job is run as an expedited job. | -| num_previous_attempts | TIMESTAMP | Number of previous attempts at running job. | -| requested_priority | LONG | The requested priority at which the job should run. | -| standby_bucket | STRING | The job's standby bucket (one of: Active, Working Set, Frequent, Rare, Never, Restricted, Exempt). | -| is_periodic | BOOL | Job should run in intervals. | -| has_flex_constraint | BOOL | True if the job should run as a flex job. | -| is_requested_as_user_initiated_job | BOOL | True is app has requested that a job be run as a user initiated job. | -| is_running_as_user_initiated_job | BOOL | True if job is running as a user initiated job. | -| deadline_ms | LONG | Deadline that job has requested and valid if has_deadline_constraint is true. | -| job_start_latency_ms | LONG | The latency in ms between when a job is scheduled and when it actually starts. | -| num_uncompleted_work_items | LONG | Number of uncompleted job work items. | -| proc_state | STRING | Process state of the process responsible for running the job. | -| internal_stop_reason | STRING | Internal stop reason for a job. | -| public_stop_reason | STRING | Public stop reason for a job. | - -**android_job_scheduler_with_screen_charging_states** - -TABLE -This table returns the constraint, charging, -and screen state changes that a job will go through -in a single trace. - -Values from this table are derived from -the `ScheduledJobStateChanged` atom. This differs from the -`android_job_scheduler_events` table in the `android.job_scheduler` module -which is derived from ATrace the system server category -(`atrace_categories: "ss"`). - -This also differs from the `android_job_scheduler_states` in this module -(`android.job_scheduler_states`) by providing charging and screen state -changes. - -To populate this table, enable the Statsd Tracing Config with the -ATOM_SCHEDULED_JOB_STATE_CHANGED push atom id. - - -This table is preferred over `android_job_scheduler_events` -since it contains more information and should be used whenever -`ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in a trace. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp of job. | -| dur | DURATION | Duration of slice in ns. | -| slice_id | JOINID(slice.id) | Id of the slice. | -| job_name | STRING | Name of the job (as named by the app). | -| job_id | LONG | Id of job (assigned by app for T- builds and system generated in U+ builds). | -| uid | LONG | Uid associated with job. | -| job_dur | DURATION | Duration of entire job in ns. | -| package_name | STRING | Package that the job belongs (ex: associated app). | -| job_namespace | STRING | Namespace of job. | -| charging_state | STRING | Device charging state during job (one of: Charging, Discharging, Not charging, Full, Unknown). | -| screen_state | STRING | Device screen state during job (one of: Screen off, Screen on, Always-on display (doze), Unknown). | -| effective_priority | LONG | Priority at which JobScheduler ran the job. | -| has_battery_not_low_constraint | BOOL | True if app requested job should run when the device battery is not low. | -| has_charging_constraint | BOOL | True if app requested job should run when the device is charging. | -| has_connectivity_constraint | BOOL | True if app requested job should run when device has connectivity. | -| has_content_trigger_constraint | BOOL | True if app requested job should run when there is a content trigger. | -| has_deadline_constraint | BOOL | True if app requested there is a deadline by which the job should run. | -| has_idle_constraint | BOOL | True if app requested job should run when device is idle. | -| has_storage_not_low_constraint | BOOL | True if app requested job should run when device storage is not low. | -| has_timing_delay_constraint | BOOL | True if app requested job has a timing delay. | -| is_prefetch | BOOL | True if app requested job should run within hours of app launch. | -| is_requested_expedited_job | BOOL | True if app requested that the job is run as an expedited job. | -| is_running_as_expedited_job | BOOL | The job is run as an expedited job. | -| num_previous_attempts | TIMESTAMP | Number of previous attempts at running job. | -| requested_priority | LONG | The requested priority at which the job should run. | -| standby_bucket | STRING | The job's standby bucket (one of: Active, Working Set, Frequent, Rare, Never, Restricted, Exempt). | -| is_periodic | BOOL | Job should run in intervals. | -| has_flex_constraint | BOOL | True if the job should run as a flex job. | -| is_requested_as_user_initiated_job | BOOL | True is app has requested that a job be run as a user initiated job. | -| is_running_as_user_initiated_job | BOOL | True if job is running as a user initiated job. | -| deadline_ms | LONG | Deadline that job has requested and valid if has_deadline_constraint is true. | -| job_start_latency_ms | LONG | The latency in ms between when a job is scheduled and when it actually starts. | -| num_uncompleted_work_items | LONG | Number of uncompleted job work items. | -| proc_state | STRING | Process state of the process responsible for running the job. | -| internal_stop_reason | STRING | Internal stop reason for a job. | -| public_stop_reason | STRING | Public stop reason for a job. | - -### android.kernel_wakelocks - -#### Views/Tables - -**android_kernel_wakelocks** - -TABLE -Table of kernel (or native) wakelocks with held duration. - -Subtracts suspended time from each period to calculate the -fraction of awake time for which the wakelock was held. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration. | -| awake_dur | DURATION | Duration spent awake. | -| name | STRING | Kernel or native wakelock name. | -| type | STRING | 'kernel' or 'native'. | -| held_dur | DURATION | Time the wakelock was held. | -| held_ratio | DOUBLE | Fraction of awake (not suspended) time the wakelock was held. | - -### android.memory.dmabuf - -#### Views/Tables - -**android_dmabuf_allocs** - -TABLE -Track dmabuf allocations, re-attributing gralloc allocations to their source -(if binder transactions to gralloc are recorded). - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | timestamp of the allocation | -| buf_size | LONG | allocation size (will be negative for release) | -| inode | LONG | dmabuf inode | -| utid | JOINID(thread.id) | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | -| tid | LONG | tid of thread responsible for the allocation | -| thread_name | STRING | thread name | -| upid | JOINID(process.id) | upid of process responsible for the allocation (matches utid) | -| pid | LONG | pid of process responsible for the allocation | -| process_name | STRING | process name | - -**android_memory_cumulative_dmabuf** - -TABLE -Provides a timeseries of dmabuf allocations for each process. -To populate this table, tracing must be enabled with the "dmabuf_allocs" ftrace event. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | upid of process responsible for the allocation (matches utid) | -| process_name | STRING | process name | -| utid | JOINID(thread.id) | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | -| thread_name | STRING | thread name | -| ts | TIMESTAMP | timestamp of the allocation | -| value | LONG | total allocation size per process and thread | - -### android.memory.heap_graph.class_relationship - -#### Macros - -**android_heap_graph_class_find_descendants** - -Given a list of classes as ancestor classes, return all the classes that -descend from them. -Returns: TableOrSubquery, Table of the schema (id JOINID(heap_graph_class.id), ancestor_class_id JOINID(heap_graph_class.id), ancestor_class_name STRING) id: `id` of the class as in heap_graph_class ancestor_class_id: `id` of the ancestor class as given in the input ancestor_class_name: `name` of the ancestor class as in heap_graph_class - -| Argument | Type | Description | -|---|---|---| -| ancestor_class_ids | TableOrSubquery | ancestor class `id`s from the heap_graph_class table containing a single column: `id` | - -### android.memory.heap_graph.class_summary_tree - -#### Views/Tables - -**android_heap_graph_class_summary_tree** - -TABLE -Table containing all the Android heap graphs in the trace converted to a -shortest-path tree and then aggregated by class name. - -This table contains a "flamegraph-like" representation of the contents of the -heap graph. - -| Column | Type | Description | -|---|---|---| -| graph_sample_ts | TIMESTAMP | The timestamp the heap graph was dumped at. | -| upid | JOINID(process.id) | The upid of the process. | -| id | LONG | The id of the node in the class tree. | -| parent_id | LONG | The parent id of the node in the class tree or NULL if this is the root. | -| name | STRING | The name of the class. | -| root_type | STRING | A string describing the type of Java root if this node is a root or NULL if this node is not a root. | -| self_count | LONG | The count of objects with the same class name and the same path to the root. | -| self_size | LONG | The size of objects with the same class name and the same path to the root. | -| cumulative_count | LONG | The sum of `self_count` of this node and all descendants of this node. | -| cumulative_size | LONG | The sum of `self_size` of this node and all descendants of this node. | - -### android.memory.heap_graph.dominator_tree - -#### Views/Tables - -**heap_graph_dominator_tree** - -TABLE -All reachable heap graph objects, their immediate dominators and summary of -their dominated sets. -The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. -Each reachable object is a node in the dominator tree, their immediate -dominator is their parent node in the tree, and their dominated set is all -their descendants in the tree. All size information come from the -heap_graph_object prelude table. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Heap graph object id. | -| idom_id | LONG | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | -| dominated_obj_count | LONG | Count of all objects dominated by this object, self inclusive. | -| dominated_size_bytes | LONG | Total self_size of all objects dominated by this object, self inclusive. | -| dominated_native_size_bytes | LONG | Total native_size of all objects dominated by this object, self inclusive. | -| depth | LONG | Depth of the object in the dominator tree. Depth of root objects are 1. | - -### android.memory.heap_graph.heap_graph_class_aggregation - -#### Views/Tables - -**android_heap_graph_class_aggregation** - -TABLE -Class-level breakdown of the java heap. -Per type name aggregates the object stats and the dominator tree stats. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Process upid | -| graph_sample_ts | TIMESTAMP | Heap dump timestamp | -| type_id | LONG | Class type id | -| type_name | STRING | Class name (deobfuscated if available) | -| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | -| obj_count | LONG | Count of class instances | -| size_bytes | LONG | Size of class instances | -| native_size_bytes | LONG | Native size of class instances | -| reachable_obj_count | LONG | Count of reachable class instances | -| reachable_size_bytes | LONG | Size of reachable class instances | -| reachable_native_size_bytes | LONG | Native size of reachable class instances | -| dominated_obj_count | LONG | Count of all objects dominated by instances of this class Only applies to reachable objects | -| dominated_size_bytes | LONG | Size of all objects dominated by instances of this class Only applies to reachable objects | -| dominated_native_size_bytes | LONG | Native size of all objects dominated by instances of this class Only applies to reachable objects | - -### android.memory.heap_graph.heap_graph_stats - -#### Views/Tables - -**android_heap_graph_stats** - -TABLE -Table summarizing java heap graphs collected with the ART perfetto module. -Contains one row per heap graph, with summary statistics (e.g. total / reachable objects) -and memory stats for the corresponding process at the time of the heap dump. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | The upid of the process. | -| graph_sample_ts | TIMESTAMP | The timestamp the heap graph was dumped at. | -| process_uptime | LONG | The uptime of the process at the time of the heap graph dump. | -| total_heap_size | LONG | The total (reachable + unreachable) size of the Java heap in bytes. | -| total_native_alloc_registry_size | LONG | The total size of native allocations (registered with NativeAllocationRegistry) in bytes. Does *not* overlap with total_heap_size. | -| total_obj_count | LONG | The number of objects in the heap. | -| reachable_heap_size | LONG | The reachable size of the Java heap in bytes. | -| reachable_native_alloc_registry_size | LONG | The size of reachable native allocations (registered with NativeAllocationRegistry) in bytes. | -| reachable_obj_count | LONG | The number of reachable objects in the heap. | -| oom_score_adj | LONG | The OOM score adj of the process at the time of the heap graph dump. | -| anon_rss_and_swap_size | LONG | The anon RSS + swap size of the process (in bytes) at the time of the heap graph dump. | -| dmabuf_rss_size | LONG | The dmabuf size of the process (in bytes) at the time of the heap graph dump. | - -### android.memory.heap_profile.summary_tree - -#### Views/Tables - -**android_heap_profile_summary_tree** - -TABLE -Table summarising the amount of memory allocated by each -callstack as seen by Android native heap profiling (i.e. -profiling information collected by heapprofd). - -> [!NOTE] -> **Note:** this table collapses data from all processes together into a single table. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | -| parent_id | LONG | The id of the parent callstack for this callstack. | -| name | STRING | The function name of the frame for this callstack. | -| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | -| source_file | STRING | The name of the file containing the function. | -| line_number | LONG | The line number in the file the function is located at. | -| self_size | LONG | The amount of memory allocated and *not freed* with this function as the leaf frame. | -| cumulative_size | LONG | The amount of memory allocated and *not freed* with this function appearing anywhere on the callstack. | -| self_alloc_size | LONG | The amount of memory allocated with this function as the leaf frame. This may include memory which was later freed. | -| cumulative_alloc_size | LONG | The amount of memory allocated with this function appearing anywhere on the callstack. This may include memory which was later freed. | - -### android.memory.lmk - -#### Views/Tables - -**android_lmk_events** - -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | timestamp of the kill being requested by lmkd | -| upid | JOINID(process.id) | upid of the process being killed | -| pid | LONG | pid of the process being killed | -| process_name | STRING | process name of the process being killed | -| oom_score_adj | LONG | oom_score_adj of the process being killed | -| kill_reason | STRING | lmkd kill_reason (matches lmkd/statslog.h kill_reasons enum) | -| kill_reason_raw | LONG | lmkd kill_reason enum value | - -### android.memory.process - -#### Views/Tables - -**memory_oom_score_with_rss_and_swap_per_process** - -TABLE -Process memory and it's OOM adjuster scores. Detects transitions, each new -interval means that either the memory or OOM adjuster score of the process changed. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp the oom_adj score or memory of the process changed | -| dur | DURATION | Duration until the next oom_adj score or memory change of the process. | -| score | LONG | oom adjuster score of the process. | -| bucket | STRING | oom adjuster bucket of the process. | -| upid | JOINID(process.id) | Upid of the process having an oom_adj update. | -| process_name | STRING | Name of the process having an oom_adj update. | -| pid | LONG | Pid of the process having an oom_adj update. | -| oom_adj_id | JOINID(slice.id) | Slice of the latest oom_adj update in the system_server. | -| oom_adj_ts | TIMESTAMP | Timestamp of the latest oom_adj update in the system_server. | -| oom_adj_dur | DURATION | Duration of the latest oom_adj update in the system_server. | -| oom_adj_track_id | JOINID(track.id) | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | -| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | -| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | -| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | -| anon_rss | LONG | Anon RSS counter value | -| file_rss | LONG | File RSS counter value | -| shmem_rss | LONG | Shared memory RSS counter value | -| rss | LONG | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | -| swap | LONG | Swap counter value | -| anon_rss_and_swap | LONG | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | -| rss_and_swap | LONG | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | - -### android.monitor_contention - -#### Views/Tables - -**android_monitor_contention** - -TABLE -Contains parsed monitor contention slices. - -| Column | Type | Description | -|---|---|---| -| blocking_method | STRING | Name of the method holding the lock. | -| blocked_method | STRING | Blocked_method without arguments and return types. | -| short_blocking_method | STRING | Blocking_method without arguments and return types. | -| short_blocked_method | STRING | Blocked_method without arguments and return types. | -| blocking_src | STRING | File location of blocking_method in form . | -| blocked_src | STRING | File location of blocked_method in form . | -| waiter_count | LONG | Zero indexed number of threads trying to acquire the lock. | -| blocked_utid | JOINID(thread.id) | Utid of thread holding the lock. | -| blocked_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_utid | JOINID(thread.id) | Utid of thread holding the lock. | -| blocking_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_tid | LONG | Tid of thread holding the lock. | -| upid | JOINID(process.id) | Upid of process experiencing lock contention. | -| process_name | STRING | Process name of process experiencing lock contention. | -| id | LONG | Slice id of lock contention. | -| ts | TIMESTAMP | Timestamp of lock contention start. | -| dur | DURATION | Wall clock duration of lock contention. | -| monotonic_dur | DURATION | Monotonic clock duration of lock contention. | -| track_id | JOINID(track.id) | Thread track id of blocked thread. | -| is_blocked_thread_main | LONG | Whether the blocked thread is the main thread. | -| blocked_thread_tid | LONG | Tid of the blocked thread | -| is_blocking_thread_main | LONG | Whether the blocking thread is the main thread. | -| blocking_thread_tid | LONG | Tid of thread holding the lock. | -| binder_reply_id | LONG | Slice id of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_ts | TIMESTAMP | Timestamp of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_tid | LONG | Tid of binder reply slice if lock contention was part of a binder txn. | -| pid | LONG | Pid of process experiencing lock contention. | - -**android_monitor_contention_chain** - -TABLE -Contains parsed monitor contention slices with the parent-child relationships. - -| Column | Type | Description | -|---|---|---| -| parent_id | LONG | Id of monitor contention slice blocking this contention. | -| blocking_method | STRING | Name of the method holding the lock. | -| blocked_method | STRING | Blocked_method without arguments and return types. | -| short_blocking_method | STRING | Blocking_method without arguments and return types. | -| short_blocked_method | STRING | Blocked_method without arguments and return types. | -| blocking_src | STRING | File location of blocking_method in form . | -| blocked_src | STRING | File location of blocked_method in form . | -| waiter_count | LONG | Zero indexed number of threads trying to acquire the lock. | -| blocked_utid | JOINID(thread.id) | Utid of thread holding the lock. | -| blocked_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_utid | JOINID(thread.id) | Utid of thread holding the lock. | -| blocking_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_tid | LONG | Tid of thread holding the lock. | -| upid | JOINID(process.id) | Upid of process experiencing lock contention. | -| process_name | STRING | Process name of process experiencing lock contention. | -| id | LONG | Slice id of lock contention. | -| ts | TIMESTAMP | Timestamp of lock contention start. | -| dur | DURATION | Wall clock duration of lock contention. | -| monotonic_dur | DURATION | Monotonic clock duration of lock contention. | -| track_id | JOINID(track.id) | Thread track id of blocked thread. | -| is_blocked_thread_main | LONG | Whether the blocked thread is the main thread. | -| blocked_thread_tid | LONG | Tid of the blocked thread | -| is_blocking_thread_main | LONG | Whether the blocking thread is the main thread. | -| blocking_thread_tid | LONG | Tid of thread holding the lock. | -| binder_reply_id | LONG | Slice id of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_ts | TIMESTAMP | Timestamp of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_tid | LONG | Tid of binder reply slice if lock contention was part of a binder txn. | -| pid | LONG | Pid of process experiencing lock contention. | -| child_id | LONG | Id of monitor contention slice blocked by this contention. | - -**android_monitor_contention_chain_thread_state** - -TABLE -Contains the span join of the first waiters in the \|android_monitor_contention_chain\| with their -blocking_thread thread state. - -Note that we only span join the duration where the lock was actually held and contended. -This can be less than the duration the lock was 'waited on' when a different waiter acquired the -lock earlier than the first waiter. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice id of lock contention. | -| ts | TIMESTAMP | Timestamp of lock contention start. | -| dur | DURATION | Wall clock duration of lock contention. | -| blocking_utid | JOINID(thread.id) | Utid of the blocking | -| blocked_function | STRING | Blocked kernel function of the blocking thread. | -| state | STRING | Thread state of the blocking thread. | - -**android_monitor_contention_chain_thread_state_by_txn** - -VIEW -Aggregated thread_states on the 'blocking thread', the thread holding the lock. -This builds on the data from \|android_monitor_contention_chain\| and -for each contention slice, it returns the aggregated sum of all the thread states on the -blocking thread. - -Note that this data is only available for the first waiter on a lock. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice id of the monitor contention. | -| thread_state | STRING | A | -| thread_state_dur | DURATION | Total time the blocking thread spent in the | -| thread_state_count | LONG | Count of all times the blocking thread entered | - -**android_monitor_contention_chain_blocked_functions_by_txn** - -VIEW -Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. -This builds on the data from \|android_monitor_contention_chain\| and -for each contention, it returns the aggregated sum of all the kernel -blocked function durations on the blocking thread. - -Note that this data is only available for the first waiter on a lock. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice id of the monitor contention. | -| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | -| blocked_function_dur | DURATION | Total time the blocking thread spent in the | -| blocked_function_count | LONG | Count of all times the blocking thread executed the | - -#### Functions - -**android_extract_android_monitor_contention_blocking_thread** - -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_blocking_tid** - -Returns LONG: Blocking thread tid - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_blocking_method** - -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_short_blocking_method** - -Extracts a shortened form of the blocking method name from a slice name. -The shortened form discards the parameter and return -types. -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_blocked_method** - -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_short_blocked_method** - -Extracts a shortened form of the monitor contention blocked method name -from a slice name. The shortened form discards the parameter and return -types. -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_waiter_count** - -Returns LONG: Count of waiters on the lock - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_blocking_src** - -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -**android_extract_android_monitor_contention_blocked_src** - -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -#### Table Functions - -**android_monitor_contention_graph** - -Returns a DAG of all Java lock contentions in a process. -Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. - -| Argument | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Upid of process to generate a lock graph for. | - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of lock graph. | - -### android.network_packets - -#### Views/Tables - -**android_network_packets** - -VIEW -Android network packet events (from android.network_packets data source). - -| Column | Type | Description | -|---|---|---| -| id | ID | Id of the slice. | -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration (non-zero only in aggregate events) | -| track_name | STRING | The track name (interface and direction) | -| package_name | STRING | Traffic package source (or uid=$X if not found) | -| iface | STRING | Traffic interface name (linux interface name) | -| direction | STRING | Traffic direction ('Transmitted' or 'Received') | -| packet_count | LONG | Number of packets in this event | -| packet_length | LONG | Number of bytes in this event (wire size) | -| packet_transport | STRING | Transport used for traffic in this event | -| packet_tcp_flags | LONG | TCP flags used by tcp frames in this event | -| socket_tag | STRING | The Android traffic tag of the network socket | -| socket_uid | LONG | The Linux user id of the network socket | -| local_port | LONG | The local port number (for udp or tcp only) | -| remote_port | LONG | The remote port number (for udp or tcp only) | -| packet_icmp_type | LONG | 1-byte ICMP type identifier. | -| packet_icmp_code | LONG | 1-byte ICMP code identifier. | -| packet_tcp_flags_int | LONG | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | -| socket_tag_int | LONG | Packet's socket tag as an integer. | - -#### Macros - -**android_network_uptime_spans** - -Computes network uptime spans based on an idle timeout model. - -It is common in networking to have an interface active for some time after -use. For example, mobile networks are often connected for 10 or more seconds -after the last packet is sent or received. This macro simulates this timeout -and returns spans that approximate the underlying connected regions. -Returns: TableOrSubquery, - -| Argument | Type | Description | -|---|---|---| -| src | TableOrSubquery | A table/view/subquery containing the network events to apply the idle timeout model to. The table must contain all partition_columns, ts, dur, packet_count, and packet_length. | -| partition_columns | ColumnNameList | A parenthesized set of columns to partition the analysis by. | -| timeout | Expr | The idle timeout, expressed in nanoseconds. | - -**android_network_uptime_cost** - -Compute the per-row uptime cost of network activity. - -It is common in networking to have an interface active for some time after -use. For example, mobile networks are often connected for 10 or more seconds -after the last packet is sent or received. This macro computes a cost factor -indicating how much each row impacts the idle timer. - -For example, assuming a 10s timeout, the first packet will extend the timeout -10s in the future, and be assigned 10s of cost. If a packet arrives 4s later, -it pushes the timer an additional 4s, receiving 4s of cost. In this simple -case, cost is MIN(ts-last_packet_ts, timeout). - -The complication is that network events can be aggregates, with more than one -packet. In such cases, we end up with a span with non-zero duration, rather -than an instant, and no easy way to compute time since the last packet. - -The solution is to detect overlap regions and compute cost for the region as -a whole. The first event in each group receives the standard uptime cost as -described above. Each group has an additional cost equal to the duration of -the group which is distributed using packet count as weight. - -For example (times in seconds, no partition, and 10 second timeout): - -`ts=5, dur=0, packet_count=1 -> group=1, uptime_cost=10 -ts=7, dur=0, packet_count=1 -> group=2, uptime_cost=2 -ts=20, dur=5, packet_count=9 -> group=3, uptime_cost=14.5 -ts=22, dur=0, packet_count=1 -> group=3, uptime_cost=0.5` The third group spans ts=20 to ts=25, with a timeout at ts=35. This gives the -group a total cost of 15 which is distributed between the two rows. The 3rd -row receives 10s for being first, and 9/10 the duration cost (5\*9/10=4.5). - -The returned table schema is (id ID, uptime_cost INT64) where uptime cost is -in nanoseconds. -Returns: TableOrSubquery, - -| Argument | Type | Description | -|---|---|---| -| src | TableOrSubquery | A table/view/subquery containing the network events to apply the idle timeout model to. The table must contain all partition_columns, id, ts, dur, and packet_count. | -| partition_columns | ColumnNameList | A parenthesized set of columns to partition the analysis by. | -| timeout | Expr | The idle timeout, expressed in nanoseconds. | - -### android.oom_adjuster - -#### Views/Tables - -**android_oom_adj_intervals** - -VIEW -All oom adj state intervals across all processes along with the reason for the state update. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp the oom_adj score of the process changed | -| dur | DURATION | Duration until the next oom_adj score change of the process. | -| score | LONG | oom_adj score of the process. | -| bucket | STRING | oom_adj bucket of the process. | -| upid | JOINID(process.id) | Upid of the process having an oom_adj update. | -| process_name | STRING | Name of the process having an oom_adj update. | -| oom_adj_id | LONG | Slice id of the latest oom_adj update in the system_server. | -| oom_adj_ts | TIMESTAMP | Timestamp of the latest oom_adj update in the system_server. | -| oom_adj_dur | DURATION | Duration of the latest oom_adj update in the system_server. | -| oom_adj_track_id | JOINID(track.id) | Track id of the latest oom_adj update in the system_server | -| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | -| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | -| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | - -#### Functions - -**android_oom_adj_score_to_bucket_name** - -Converts an oom_adj score Integer to String sample name. -One of: cached, background, job, foreground_service, bfgs, foreground and -system. -Returns STRING: Returns the sample bucket based on the oom score. - -| Argument | Type | Description | -|---|---|---| -| oom_score | LONG | `oom_score` value | - -**android_oom_adj_score_to_detailed_bucket_name** - -Converts an oom_adj score Integer to String bucket name. -Deprecated: use `android_oom_adj_score_to_bucket_name` instead. -Returns STRING: Returns the oom_adj bucket. - -| Argument | Type | Description | -|---|---|---| -| value | LONG | oom_adj score. | -| android_appid | LONG | android_app id of the process. | - -### android.power_rails - -#### Views/Tables - -**android_power_rails_counters** - -TABLE -Android power rails counters data. -For details see: -NOTE: Requires dedicated hardware - table is only populated on Pixels. - -| Column | Type | Description | -|---|---|---| -| id | ID(counter.id) | `counter.id` | -| ts | TIMESTAMP | Timestamp of the energy measurement. | -| dur | DURATION | Time until the next energy measurement. | -| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | -| raw_power_rail_name | STRING | Raw power rail name. | -| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | -| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | -| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | -| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | -| track_id | JOINID(track.id) | Power rail track id. Alias of `counter_track.id`. | -| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | - -**android_power_rails_metadata** - -TABLE -High level metadata about each of the power rails. - -| Column | Type | Description | -|---|---|---| -| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | -| raw_power_rail_name | STRING | Raw power rail name from the hardware. | -| friendly_name | STRING | User-friendly name for the power rail. | -| track_id | JOINID(track.id) | Power rail track id. Alias of `counter_track.id`. | -| subsystem_name | STRING | Subsystem name that this power rail belongs to. | -| machine_id | JOINID(machine.id) | The device the power rail is associated with. | - -### android.process_metadata - -#### Views/Tables - -**android_process_metadata** - -TABLE -Data about packages running on the process. - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Process upid. | -| pid | LONG | Process pid. | -| process_name | STRING | Process name. | -| uid | LONG | Android app UID. | -| shared_uid | BOOL | Whether the UID is shared by multiple packages. | -| user_id | LONG | Android user id for multi-user devices | -| user_type | STRING | Type of the Android user (e.g., HEADLESS, SECONDARY) | -| package_name | STRING | Name of the packages running in this process. | -| version_code | LONG | Package version code. | -| debuggable | LONG | Whether package is debuggable. | -| is_kernel_task | BOOL | Whether the task is kernel or not | - -#### Functions - -**android_is_kernel_task** - -Returns true if the process is a kernel task. -Returns BOOL: True for kernel tasks - -| Argument | Type | Description | -|---|---|---| -| upid | LONG | Queried process | - -### android.screen_state - -#### Views/Tables - -**android_screen_state** - -TABLE -Table of the screen state - on, off or doze (always on display). - -| Column | Type | Description | -|---|---|---| -| id | ID | ID. | -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration. | -| simple_screen_state | STRING | Simplified screen state: 'unknown', 'off', 'doze' (AoD) or 'on' | -| short_screen_state | STRING | Full screen state, adding VR and suspended-while-displaying states. | -| screen_state | STRING | Human-readable string. | - -### android.screenshots - -#### Views/Tables - -**android_screenshots** - -TABLE -Screenshot slices, used in perfetto UI. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Id of the screenshot slice. | -| ts | TIMESTAMP | Slice timestamp. | -| dur | DURATION | Slice duration, should be typically 0 since screeenshot slices are of instant type. | -| name | STRING | Slice name. | - -### android.services - -#### Views/Tables - -**android_service_bindings** - -TABLE -All service bindings from client app to server app. - -| Column | Type | Description | -|---|---|---| -| client_oom_score | LONG | OOM score of client process making the binding. | -| client_process | STRING | Name of client process making the binding. | -| client_thread | STRING | Name of client thread making the binding. | -| client_pid | LONG | Pid of client process making the binding. | -| client_tid | LONG | Tid of client process making the binding. | -| client_upid | JOINID(process.id) | Upid of client process making the binding. | -| client_utid | JOINID(thread.id) | Utid of client thread making the binding. | -| client_ts | TIMESTAMP | Timestamp the client process made the request. | -| client_dur | DURATION | Duration of the client binding request. | -| server_oom_score | LONG | OOM score of server process getting bound to. | -| server_process | STRING | Name of server process getting bound to | -| server_thread | STRING | Name of server thread getting bound to. | -| server_pid | LONG | Pid of server process getting bound to. | -| server_tid | LONG | Tid of server process getting bound to. | -| server_upid | JOINID(process.id) | Upid of server process getting bound to. | -| server_utid | JOINID(thread.id) | Utid of server process getting bound to. | -| server_ts | TIMESTAMP | Timestamp the server process got bound to. | -| server_dur | DURATION | Duration of the server process handling the binding. | -| token | STRING | Unique binder identifier for the Service binding. | -| act | STRING | Intent action name for the service binding. | -| cmp | STRING | Intent component name for the service binding. | -| flg | STRING | Intent flag for the service binding. | -| bind_seq | LONG | Monotonically increasing id for the service binding. | - -### android.slices - -#### Functions - -**android_standardize_slice_name** - -Some slice names have params in them. This functions removes them to make it -possible to aggregate by name. -Some examples are: - -- Lock/monitor contention slices. The name includes where the lock contention is in the code. That part is removed. -- DrawFrames/ooFrame. The name also includes the frame number. -- Apk/oat/dex loading: The name of the apk is removed Returns STRING: Simplified name. - -| Argument | Type | Description | -|---|---|---| -| name | STRING | The raw slice name. | - -### android.startup.startup_breakdowns - -#### Views/Tables - -**android_startup_opinionated_breakdown** - -TABLE -Blended thread state and slice breakdown blocking app startups. - -Each row blames a unique period during an app startup with a reason -derived from the slices and thread states on the main thread. - -Some helpful events to enables are binder transactions, ART, am and view. - -| Column | Type | Description | -|---|---|---| -| startup_id | JOINID(android_startups.startup_id) | Startup id. | -| slice_id | JOINID(slice.id) | Id of relevant slice blocking startup. | -| thread_state_id | JOINID(thread_state.id) | Id of thread_state blocking startup. | -| ts | TIMESTAMP | Timestamp of an exclusive interval during the app startup with a single latency reason. | -| dur | DURATION | Duration of an exclusive interval during the app startup with a single latency reason. | -| reason | STRING | Cause of delay during an exclusive interval of the app startup. | - -### android.startup.startups - -#### Views/Tables - -**android_startup_processes** - -TABLE -Maps a startup to the set of processes that handled the activity start. - -The vast majority of cases should be a single process. However it is -possible that the process dies during the activity startup and is respawned. - -| Column | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| upid | JOINID(process.id) | Upid of process on which activity started. | -| pid | LONG | Pid of process on which activity started. | -| startup_type | STRING | Type of the startup. | - -**android_startups** - -VIEW -All activity startups in the trace by startup id. -Populated by different scripts depending on the platform version/contents. - -| Column | Type | Description | -|---|---|---| -| startup_id | ID | Startup id. | -| ts | TIMESTAMP | Timestamp of startup start. | -| ts_end | LONG | Timestamp of startup end. | -| dur | DURATION | Startup duration. | -| package | STRING | Package name. | -| startup_type | STRING | Startup type. | - -**android_startup_threads** - -VIEW -Maps a startup to the set of threads on processes that handled the -activity start. - -| Column | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| ts | TIMESTAMP | Timestamp of start. | -| dur | DURATION | Duration of startup. | -| upid | JOINID(process.id) | Upid of process involved in startup. | -| pid | LONG | Pid if process involved in startup. | -| utid | JOINID(thread.id) | Utid of the thread. | -| tid | LONG | Tid of the thread. | -| thread_name | STRING | Name of the thread. | -| is_main_thread | BOOL | Thread is a main thread. | - -**android_thread_slices_for_all_startups** - -VIEW -All the slices for all startups in trace. - -Generally, this view should not be used. Instead, use one of the view functions related -to the startup slices which are created from this table. - -| Column | Type | Description | -|---|---|---| -| startup_ts | TIMESTAMP | Timestamp of startup. | -| startup_ts_end | LONG | Timestamp of startup end. | -| startup_id | LONG | Startup id. | -| utid | JOINID(thread.id) | UTID of thread with slice. | -| tid | LONG | Tid of thread. | -| thread_name | STRING | Name of thread. | -| is_main_thread | BOOL | Whether it is main thread. | -| arg_set_id | ARGSETID | Arg set id. | -| slice_id | JOINID(slice.id) | Slice id. | -| slice_name | STRING | Name of slice. | -| slice_ts | TIMESTAMP | Timestamp of slice start. | -| slice_dur | LONG | Slice duration. | - -**android_class_loading_for_startup** - -VIEW -A Perfetto view that lists matching slices for class loading during app startup. - -| Column | Type | Description | -|---|---|---| -| slice_id | JOINID(slice.id) | Id of the slice. | -| startup_id | LONG | Startup id. | -| slice_name | STRING | Name of the slice. | -| slice_ts | TIMESTAMP | Timestamp of start of the slice. | -| slice_dur | DURATION | Duration of the slice. | -| thread_name | STRING | Name of the thread with the slice. | -| tid | LONG | Tid of the thread with the slice. | -| arg_set_id | ARGSETID | Arg set id. | - -#### Functions - -**android_sum_dur_for_startup_and_slice** - -Returns duration of startup for slice name. - -Sums duration of all slices of startup with provided name. -Returns LONG: Sum of duration. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| slice_name | STRING | Slice name. | - -**android_sum_dur_on_main_thread_for_startup_and_slice** - -Returns duration of startup for slice name on main thread. - -Sums duration of all slices of startup with provided name only on main thread. -Returns LONG: Sum of duration. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| slice_name | STRING | Slice name. | - -#### Table Functions - -**android_slices_for_startup_and_slice_name** - -Given a startup id and GLOB for a slice name, returns matching slices with data. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| slice_name | STRING | Glob of the slice. | - -| Column | Type | Description | -|---|---|---| -| slice_id | JOINID(slice.id) | Id of the slice. | -| slice_name | STRING | Name of the slice. | -| slice_ts | TIMESTAMP | Timestamp of start of the slice. | -| slice_dur | DURATION | Duration of the slice. | -| thread_name | STRING | Name of the thread with the slice. | -| tid | LONG | Tid of the thread with the slice. | -| arg_set_id | ARGSETID | Arg set id. | - -**android_binder_transaction_slices_for_startup** - -Returns binder transaction slices for a given startup id with duration over threshold. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| threshold | DOUBLE | Only return slices with duration over threshold. | - -| Column | Type | Description | -|---|---|---| -| id | LONG | Slice id. | -| slice_dur | DURATION | Slice duration. | -| thread_name | STRING | Name of the thread with slice. | -| process | STRING | Name of the process with slice. | -| arg_set_id | ARGSETID | Arg set id. | -| is_main_thread | BOOL | Whether is main thread. | - -### android.startup.time_to_display - -#### Views/Tables - -**android_startup_time_to_display** - -TABLE -Startup metric defintions, which focus on the observable time range: -TTID - Time To Initial Display - -- -- end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display -- -- end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. - -| Column | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| time_to_initial_display | LONG | Time to initial display (TTID) | -| time_to_full_display | LONG | Time to full display (TTFD) | -| ttid_frame_id | LONG | `android_frames.frame_id` of frame for initial display | -| ttfd_frame_id | LONG | `android_frames.frame_id` of frame for full display | -| upid | JOINID(process.id) | `process.upid` of the startup | - -### android.statsd - -#### Views/Tables - -**android_statsd_atoms** - -VIEW -Statsd atoms. - -A subset of the slice table containing statsd atom instant events. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Unique identifier for this slice. | -| ts | TIMESTAMP | The timestamp at the start of the slice. | -| dur | DURATION | The duration of the slice. | -| arg_set_id | ARGSETID | The id of the argument set associated with this slice. | -| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| track_id | JOINID(track.id) | The id of the track this slice is located on. | -| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | -| name | STRING | The name of the slice. The name describes what was happening during the slice. | -| depth | LONG | The depth of the slice in the current stack of slices. | -| parent_id | LONG | The id of the parent (i.e. immediate ancestor) slice for this slice. | -| thread_ts | TIMESTAMP | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | - -### android.surfaceflinger - -#### Views/Tables - -**android_app_to_sf_frame_timeline_match** - -TABLE -Match the frame timeline on the app side with the frame timeline on the SF side. -In cases where there are multiple layers drawn, there would be separate frame timeline -slice for each of the layers. GROUP BY is used to deduplicate these rows. - -| Column | Type | Description | -|---|---|---| -| app_upid | JOINID(process.upid) | upid of the app. | -| app_vsync | LONG | vsync id of the app. | -| sf_upid | JOINID(process.upid) | upid of surfaceflinger process. | -| sf_vsync | LONG | vsync id for surfaceflinger. | - -### android.suspend - -#### Views/Tables - -**android_suspend_state** - -TABLE -Table of suspended and awake slices. - -Selects either the minimal or full ftrace source depending on what's -available, marks suspended periods, and complements them to give awake -periods. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| power_state | STRING | 'awake' or 'suspended' | - -### android.thread - -#### Functions - -**android_standardize_thread_name** - -Standardizes an Android thread name by extracting its core identifier to make it -possible to aggregate by name. - -Removes extra parts of a thread name, like identifiers, leaving only the main prefix. -Splits the name at ('-', '\[', ':' , ' '). - -Some Examples: -Given thread_name = "RenderThread-1\[123\]", -returns "RenderThread". - -Given thread_name = "binder:5543_E" -returns "binder". - -Given thread_name = "pool-3-thread-5", -returns "pool". - -Given thread_name = "MainThread", -returns "MainThread". -Returns STRING: Simplified name - -| Argument | Type | Description | -|---|---|---| -| thread_name | STRING | The full android thread name to be processed. | - -### android.user_list - -#### Views/Tables - -**android_user_list** - -VIEW -Contains information about Android users in the trace. - -This is populated by the `android.user_list` data-source which lives in -traced_probes and is available on default from 26Q2+ devices. - -> [!NOTE] -> **Note:** `users` here corresponds to Android users *not* Linux users. So this is not about Linux uids (which in Android correspons to different apps). - -| Column | Type | Description | -|---|---|---| -| android_user_id | LONG | The Android user id. Often useful to join with `android_process_metadata.user_id` | -| type | STRING | A string "enum" indicating the type of the user according to the system. Will be one for a few constant values e.g. HEADLESS, SECONDARY, GUEST. | - -### android.wakeups - -#### Views/Tables - -**android_wakeups** - -TABLE -Table of parsed wakeup / suspend failure events with suspend backoff. - -Certain wakeup events may have multiple causes. When this occurs we -split those causes into multiple rows in this table with the same ts -and raw_wakeup values. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | -| dur | DURATION | Duration for which we blame the wakeup for wakefulness. This is the suspend backoff duration if one exists, or the lesser of (5 seconds, time to next suspend event). | -| raw_wakeup | STRING | Original wakeup string from the kernel. | -| on_device_attribution | STRING | Wakeup attribution, as determined on device. May be absent. | -| type | STRING | One of 'normal' (device woke from sleep), 'abort_pending' (suspend failed due to a wakeup that was scheduled by a device during the suspend process), 'abort_last_active' (suspend failed, listing the last active device) or 'abort_other' (suspend failed for another reason). | -| item | STRING | Individual wakeup cause. Usually the name of the device that cause the wakeup, or the raw message in the 'abort_other' case. | -| suspend_quality | STRING | 'good' or 'bad'. 'bad' means failed or short such that suspend backoff is triggered. | -| backoff_state | STRING | 'new', 'continue' or NULL. Set if suspend backoff is triggered. | -| backoff_reason | STRING | 'short', 'failed' or NULL. Set if suspend backoff is triggered. | -| backoff_count | LONG | Number of times suspend backoff has occurred, or NULL. Set if suspend backoff is triggered. | -| backoff_millis | LONG | Next suspend backoff duration, or NULL. Set if suspend backoff is triggered. | - -### android.winscope.inputmethod - -#### Views/Tables - -**android_inputmethod_clients** - -VIEW -Android inputmethod clients state dumps (from android.inputmethod data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Dump id | -| ts | TIMESTAMP | Timestamp when the dump was triggered | -| arg_set_id | ARGSETID | Extra args parsed from the proto message | - -**android_inputmethod_manager_service** - -VIEW -Android inputmethod manager service state dumps (from android.inputmethod data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Dump id | -| ts | TIMESTAMP | Timestamp when the dump was triggered | -| arg_set_id | ARGSETID | Extra args parsed from the proto message | - -**android_inputmethod_service** - -VIEW -Android inputmethod service state dumps (from android.inputmethod data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Dump id | -| ts | TIMESTAMP | Timestamp when the dump was triggered | -| arg_set_id | ARGSETID | Extra args parsed from the proto message | - -### android.winscope.rect - -#### Views/Tables - -**android_winscope_rect** - -VIEW -Android Winscope rects. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Rect id | -| x | DOUBLE | x | -| y | DOUBLE | y | -| w | DOUBLE | w | -| h | DOUBLE | h | - -**android_winscope_transform** - -VIEW -Android Winscope transforms. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Transform id | -| dsdx | DOUBLE | dsdx | -| dtdx | DOUBLE | dtdx | -| tx | DOUBLE | tx | -| dtdy | DOUBLE | dtdy | -| dsdy | DOUBLE | dsdy | -| ty | DOUBLE | ty | - -**android_winscope_trace_rect** - -VIEW -Android Winscope trace rects. - -| Column | Type | Description | -|---|---|---| -| id | LONG | Trace rect id | -| rect_id | LONG | Rect id | -| group_id | LONG | Group id | -| depth | LONG | Depth | -| is_spy | LONG | Is spy rect | -| is_visible | LONG | Is visible | -| opacity | DOUBLE | Opacity | -| transform_id | LONG | Transform id | -| border_width | LONG | Border width | -| border_color | DOUBLE | Border color | - -### android.winscope.surfaceflinger - -#### Views/Tables - -**android_surfaceflinger_transaction** - -VIEW -Android surfaceflinger transactions (from android.surfaceflinger.transactions data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Row id | -| snapshot_id | LONG | Snapshot id | -| arg_set_id | LONG | Arg set id | -| transaction_id | LONG | Transaction id | -| pid | LONG | PID | -| uid | LONG | UID | -| layer_id | LONG | Layer id | -| display_id | LONG | Display id | -| flags_id | LONG | Flags id | -| transaction_type | STRING | Transaction type | - -**android_surfaceflinger_transaction_flag** - -VIEW -Android surfaceflinger transaction flags. - -| Column | Type | Description | -|---|---|---| -| flags_id | LONG | Flags id | -| flag | STRING | Flag | - -**android_surfaceflinger_display** - -VIEW -Android surfaceflinger displays (from android.surfaceflinger.layers data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Id | -| snapshot_id | LONG | Snapshot id | -| is_on | LONG | Is on | -| is_virtual | LONG | Is virtual | -| trace_rect_id | LONG | Trace rect id | -| display_id | LONG | Display id | -| display_name | STRING | Display name | - -**android_winscope_fill_region** - -VIEW -Android surfaceflinger input rect fill regions (from android.surfaceflinger.layers data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Fill region id | -| trace_rect_id | LONG | Trace rect id | -| rect_id | LONG | Rect id | - -### android.winscope.transitions - -#### Views/Tables - -**android_window_manager_shell_transition_participants** - -VIEW -Android transition participants (from com.android.wm.shell.transition data source). - -| Column | Type | Description | -|---|---|---| -| transition_id | LONG | Transition id | -| layer_id | LONG | Layer participant | -| window_id | LONG | Window participant | - -**android_window_manager_shell_transition_protos** - -VIEW -Android transition protos (from com.android.wm.shell.transition data source). - -| Column | Type | Description | -|---|---|---| -| transition_id | LONG | Transition id | -| base64_proto_id | LONG | Base64 proto id | - -### android.winscope.viewcapture - -#### Views/Tables - -**android_viewcapture** - -VIEW -Android viewcapture (from android.viewcapture data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Snapshot id | -| ts | TIMESTAMP | Timestamp when snapshot was triggered | -| arg_set_id | ARGSETID | Extra args parsed from proto message | -| package_name | STRING | Package name deinterned from proto message | -| window_name | STRING | Window name deinterned from proto message | - -**android_viewcapture_view** - -VIEW -Android viewcapture view (from android.viewcapture data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Row id | -| snapshot_id | LONG | Snapshot id | -| arg_set_id | ARGSETID | Extra args parsed from proto message | -| node_id | LONG | Id from proto message | -| hashcode | LONG | Hashcode from proto message | -| is_visible | LONG | Is visible computed from proto message and view position | -| parent_id | LONG | Parent id from proto message | -| view_id | STRING | View id deinterned from proto message | -| class_name | STRING | Class name deinterned from proto message | -| trace_rect_id | LONG | Trace rect id | - -### android.winscope.windowmanager - -#### Views/Tables - -**android_windowmanager** - -VIEW -Android WindowManager (from android.windowmanager data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Snapshot id | -| ts | TIMESTAMP | Timestamp when the snapshot was triggered | -| arg_set_id | ARGSETID | Extra args parsed from the proto message | -| base64_proto_id | LONG | Raw proto message | -| focused_display_id | LONG | Focused display id for this snapshot | -| has_invalid_elapsed_ts | BOOL | Indicates whether snapshot was recorded without elapsed timestamp | - -**android_windowmanager_windowcontainer** - -VIEW -Android WindowManager WindowContainer (from android.windowmanager data source). - -| Column | Type | Description | -|---|---|---| -| id | LONG | Row id | -| snapshot_id | LONG | Snapshot id | -| arg_set_id | ARGSETID | Extra args parsed from the proto message | -| base64_proto_id | LONG | Raw proto message | -| title | STRING | The window container's title | -| token | LONG | The window container's token | -| parent_token | LONG | The parent window container's token | -| child_index | LONG | The index of this window container within the parent's children | -| is_visible | BOOL | The window container visibility | -| window_rect_id | LONG | The rect corresponding to this window container | -| container_type | STRING | The window container type e.g. DisplayContent, TaskFragment | -| name_override | STRING | Optional name override for some container types | +
## Package: v8 @@ -5042,7 +693,11 @@ Android WindowManager WindowContainer (from android.windowmanager data source). #### Views/Tables -**v8_isolate** +
+ +**v8_isolate**. A V8 Isolate instance + +
VIEW A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8 @@ -5050,9 +705,9 @@ engine. | Column | Type | Description | |---|---|---| -| v8_isolate_id | LONG | Unique V8 isolate id. | -| upid | JOINID(process.id) | Process the isolate was created in. | -| internal_isolate_id | LONG | Internal id used by the v8 engine. Unique in a process. | +| v8_isolate_id | UINT | Unique V8 isolate id. | +| upid | UINT | Process the isolate was created in. | +| internal_isolate_id | UINT | Internal id used by the v8 engine. Unique in a process. | | embedded_blob_code_start_address | LONG | Absolute start address of the embedded code blob. | | embedded_blob_code_size | LONG | Size in bytes of the embedded code blob. | | code_range_base_address | LONG | Base address of the code range if the isolate defines one. | @@ -5060,7 +715,15 @@ engine. | shared_code_range | LONG | Whether the code range for this Isolate is shared with others in the same process. There is at max one such shared code range per process. | | embedded_blob_code_copy_start_address | LONG | Used when short builtin calls are enabled, where embedded builtins are copied into the CodeRange so calls can be nearer. | -**v8_js_script** +
+ +
+ +
+ +**v8_js_script**. Represents a script that was compiled to generate code + +
VIEW Represents a script that was compiled to generate code. Some V8 code is @@ -5069,86 +732,592 @@ will not (e.g. builtins). | Column | Type | Description | |---|---|---| -| v8_js_script_id | LONG | Unique V8 JS script id. | -| v8_isolate_id | LONG | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | -| internal_script_id | LONG | Script id used by the V8 engine. | +| v8_js_script_id | UINT | Unique V8 JS script id. | +| v8_isolate_id | UINT | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | | script_type | STRING | Script type. | | name | STRING | Script name. | | source | STRING | Actual contents of the script. | -**v8_wasm_script** +
+ +
+ +
+ +**v8_wasm_script**. Represents one WASM script. + +
VIEW -Represents one WASM script. | Column | Type | Description | |---|---|---| -| v8_wasm_script_id | LONG | Unique V8 WASM script id. | -| v8_isolate_id | LONG | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | -| internal_script_id | LONG | Script id used by the V8 engine. | +| v8_wasm_script_id | UINT | Unique V8 WASM script id. | +| v8_isolate_id | UINT | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | | url | STRING | URL of the source. | -| wire_bytes | BYTES | Raw wire bytes of the script. | -| source | STRING | Actual source code of the script. | +| source | STRING | Actual contents of the script. | -**v8_js_function** +
+ +
+ +
+ +**v8_js_function**. Represents a v8 Javascript function. + +
VIEW -Represents a v8 Javascript function. | Column | Type | Description | |---|---|---| -| v8_js_function_id | LONG | Unique V8 JS function id. | +| v8_js_function_id | UINT | Unique V8 JS function id. | | name | STRING | Function name. | -| v8_js_script_id | LONG | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | +| v8_js_script_id | UINT | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | | is_toplevel | BOOL | Whether this function represents the top level script. | | kind | STRING | Function kind (e.g. regular function or constructor). | -| line | LONG | Line in script where function is defined. Starts at 1. | -| col | LONG | Column in script where function is defined. Starts at 1. | +| line | UINT | Line in script where function is defined. Starts at 1. | +| col | UINT | Column in script where function is defined. Starts at 1. | -## Package: intervals +
-### intervals.intersect +
-#### Macros +## Package: wattson -**interval_self_intersect** +### wattson.system_state -Given a list of intervals (ts, dur), this macro generates a list of interval -end points as well as the intervals that intersect with those points. +#### Views/Tables -e.g. input (10, 20), (20, 25) +
-`10 30 -A |---| -B|---| -20 45` would generate the output: +**wattson_system_states**. The final system state for the CPU subsystem, which has all the information needed by Wattson to estimate energy for the CPU subsystem. -`ts,dur,group_id,id,interval_ends_at_ts -10,10,1,A,0 -20,10,2,A,0 -20,10,2,B,0 -30,15,3,A,1 -30,15,3,B,0 -45,0,4,B,1` Runtime is O(n log n + m), where n is the number of intervals and m -is the size of the output. -Returns: TableOrSubquery, +
+ +TABLE +The final system state for the CPU subsystem, which has all the information +needed by Wattson to estimate energy for the CPU subsystem. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Starting timestamp of the current counter where system state is constant. | +| dur | INT | Duration of the current counter where system state is constant. | +| l3_hit_count | INT | Number of L3 hits the current system state. | +| l3_miss_count | INT | Number of L3 misses in the current system state. | +| freq_0 | INT | Frequency of CPU0. | +| idle_0 | INT | Idle state of CPU0. | +| freq_1 | INT | Frequency of CPU1. | +| idle_1 | INT | Idle state of CPU1. | +| freq_2 | INT | Frequency of CPU2. | +| idle_2 | INT | Idle state of CPU2. | +| freq_3 | INT | Frequency of CPU3. | +| idle_3 | INT | Idle state of CPU3. | +| freq_4 | INT | Frequency of CPU4. | +| idle_4 | INT | Idle state of CPU4. | +| freq_5 | INT | Frequency of CPU5. | +| idle_5 | INT | Idle state of CPU5. | +| freq_6 | INT | Frequency of CPU6. | +| idle_6 | INT | Idle state of CPU6. | +| freq_7 | INT | Frequency of CPU7. | +| idle_7 | INT | Idle state of CPU7. | +| suspended | BOOL | Flag indicating if current system state is suspended. | + +
+ +
+ +## Package: stacks + +### stacks.cpu_profiling + +#### Views/Tables + +
+ +**cpu_profiling_samples**. Table containing all the timestamped samples of CPU profiling which occurred during the trace. Currently, this table is backed by the following data sources: \* Linux perf \* macOS instruments \* Chrome CPU profiling \* Legacy V8 CPU profiling \* Profiling data in Gecko traces + +
+ +TABLE +Table containing all the timestamped samples of CPU profiling which occurred +during the trace. + +Currently, this table is backed by the following data sources: +\* Linux perf +\* macOS instruments +\* Chrome CPU profiling +\* Legacy V8 CPU profiling +\* Profiling data in Gecko traces + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the sample. | +| ts | INT | The timestamp of the sample. | +| utid | INT | The utid of the thread of the sample, if available. | +| tid | INT | The tid of the sample, if available. | +| thread_name | STRING | The thread name of thread of the sample, if available. | +| ucpu | INT | The ucpu of the sample, if available. | +| cpu | INT | The cpu of the sample, if available. | +| callsite_id | INT | The callsite id of the sample. | + +
+ +
+ +
+ +**cpu_profiling_summary_tree** . Table summarising the callstacks captured during any CPU profiling which occurred during the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. The data sources supported are the same as the `cpu_profiling_samples` table. + +
+ +TABLE +Table summarising the callstacks captured during any CPU profiling which +occurred during the trace. + +Specifically, this table returns a tree containing all the callstacks seen +during the trace with `self_count` equal to the number of samples with that +frame as the leaf and `cumulative_count` equal to the number of samples with +the frame anywhere in the tree. + +The data sources supported are the same as the `cpu_profiling_samples` table. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | +| parent_id | INT | The id of the parent callstack for this callstack. NULL if this is root. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
+ +## Package: pkvm + +### pkvm.hypervisor + +#### Views/Tables + +
+ +**pkvm_hypervisor_events**. Events when CPU entered hypervisor. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the corresponding slice in slices table. | +| cpu | INT | CPU that entered hypervisor. | +| ts | INT | Timestamp when CPU entered hypervisor (in nanoseconds). | +| dur | INT | How much time CPU spent in hypervisor (in nanoseconds). | +| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | + +
+ +
+ +## Package: slices + +### slices.cpu_time + +#### Views/Tables + +
+ +**thread_slice_cpu_time**. Time each thread slice spent running on CPU. Requires scheduling data to be available in the trace. + +
+ +TABLE +Time each thread slice spent running on CPU. +Requires scheduling data to be available in the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| cpu_time | INT | Duration of the time the slice was running. | + +
+ +
+ +
+ +**thread_slice_cpu_cycles**. CPU cycles per each slice. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +### slices.with_context + +#### Views/Tables + +
+ +**thread_slice**. All thread slices with data about thread, thread track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All thread slices with data about thread, thread track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `thread_track.name`. | +| thread_name | STRING | Alias for `thread.name`. | +| utid | INT | Alias for `thread.utid`. | +| tid | INT | Alias for `thread.tid`. | +| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +
+ +**process_slice**. All process slices with data about process track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All process slices with data about process track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `process_track.name`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +## Package: sched + +### sched.runnable + +#### Views/Tables + +
+ +**sched_previous_runnable_on_thread**. Previous runnable slice on the same thread. For each "Running" thread state finds: - previous "Runnable" (or runnable preempted) state. - previous uninterrupted "Runnable" state with a valid waker thread. + +
+ +TABLE +Previous runnable slice on the same thread. +For each "Running" thread state finds: +- previous "Runnable" (or runnable preempted) state. +- previous uninterrupted "Runnable" state with a valid waker thread. + +| Column | Type | Description | +|---|---|---| +| id | INT | `thread_state.id` id. | +| prev_runnable_id | INT | Previous runnable `thread_state.id`. | +| prev_wakeup_runnable_id | INT | Previous runnable `thread_state.id` with valid waker thread. | + +
+ +
+ +### sched.states + +#### Functions + +
+ +**sched_state_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state to a human-readable string. + +
+ +
+ +
+ +Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | Argument | Type | Description | |---|---|---| -| intervals | TableOrSubquery | Table or subquery containing interval data. | +| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | + +
+ +
+ +
+ +**sched_state_io_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state and IO wait information to a human-readable string. + +
+ +
+ +
+ +Translates a single-letter scheduling state and IO wait information to +a human-readable string. +Returns STRING: A human readable string with information about the scheduling state and IO wait. + +| Argument | Type | Description | +|---|---|---| +| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | + +
+ +
+ +### sched.thread_level_parallelism + +#### Views/Tables + +
+ +**sched_runnable_thread_count**. The count of runnable threads over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the runnable thread count changed to the current value. | +| runnable_thread_count | INT | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +
+ +**sched_active_cpu_count**. The count of active CPUs over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the number of active CPU changed. | +| active_cpu_count | INT | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +### sched.time_in_state + +#### Views/Tables + +
+ +**sched_time_in_state_for_thread**. The time a thread spent in each scheduling state during it's lifetime. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| total_runtime | INT | Total runtime of thread. | +| state | STRING | One of the scheduling states of kernel thread. | +| time_in_state | INT | Total time spent in the scheduling state. | +| percentage_in_state | INT | Percentage of time thread spent in scheduling state in \[0-100\] range. | + +
+ +
+ +
+ +**sched_percentage_of_time_in_state**. Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] ranges) + +
+ +TABLE +Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] +ranges). Sum of all states might be smaller than 100, as those values +are rounded down. + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| running | INT | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | +| runnable | INT | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | +| runnable_preempted | INT | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | +| sleeping | INT | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | +| uninterruptible_sleep | INT | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | +| other | INT | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | + +
+ +
+ +#### Table Functions + +
+ +**sched_time_in_state_for_thread_in_interval** . Time the thread spent each state in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_and_cpu_for_thread_in_interval** . Time the thread spent each state and cpu in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| cpu | INT | Id of the CPU. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_for_cpu_in_interval** . Time spent by CPU in each scheduling state in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| cpu | INT | CPU id. | +| ts | INT | Interval start. | +| dur | INT | Interval duration. | + +| Column | Type | Description | +|---|---|---| +| end_state | STRING | End state. From `sched.end_state`. | +| dur | INT | Duration in state. | + +
+ +
+ +## Package: intervals ### intervals.overlap #### Macros -**intervals_overlap_count** +
+ +**intervals_overlap_count** . Compute the distribution of the overlap of the given intervals over time. Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) counter, with the value corresponding to the number of intervals that overlap the given timestamp and interval until the next timestamp. + +
+ +
+ +
Compute the distribution of the overlap of the given intervals over time. Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) counter, with the value corresponding to the number of intervals that overlap the given timestamp and interval until the next timestamp. -Returns: TableOrSubquery, The returned table has the schema (ts TIMESTAMP, value LONG). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. +Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. | Argument | Type | Description | |---|---|---| @@ -5156,138 +1325,337 @@ Returns: TableOrSubquery, The returned table has the schema (ts TIMESTAMP, value | ts_column | ColumnName | Column containing interval starts (usually `ts`). | | dur_column | ColumnName | Column containing interval durations (usually `dur`). | -**intervals_overlap_count_by_group** +
-Compute the distribution of the overlap of the given intervals over time from -slices in a same group. +
-Each interval is a (ts, dur, group) triple and the overlap represented as a -(ts, value, group) counter, with the value corresponding to the number of -intervals that belong to the same group and overlap the given timestamp and -interval until the next timestamp. -Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32, group_name) where the type of group_name is the same as that in \|segments\|. \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. \|group_name\| is the name of a group used for the overlap calculation. +## Package: time -| Argument | Type | Description | -|---|---|---| -| segments | TableOrSubquery | Table or subquery containing interval data. | -| ts_column | ColumnName | Column containing interval starts (usually `ts`). | -| dur_column | ColumnName | Column containing interval durations (usually `dur`). | -| group_column | ColumnName | Column containing group name for grouping. | - -**interval_merge_overlapping** - -Merge intervals when they overlap to generate a minimum covering set of -intervals with no overlap. The intervals are closed (contain both endpoints) -and we consider two intervals overlapping -(a) the intervals overlap or -(b) if the end point of one interval is within epsilon of the start point -of the other. -Returns: TableOrSubquery, - -| Argument | Type | Description | -|---|---|---| -| intervals | TableOrSubquery | Table or subquery containing interval data. | -| epsilon | Expr | Constant expression for a tolerance in testing overlap (usually `0`) | - -**interval_merge_overlapping_partitioned** - -Merge overlapping intervals within each partition group to generate a minimum -covering set of intervals with no overlap within each partition. - -For each partition, this macro merges overlapping intervals into -non-overlapping intervals. The result contains intervals where at least -one input interval is active. - -For example, with partition 'A': -Input: (ts=1, dur=10), (ts=5, dur=12) -Output: (ts=1, dur=16) -Returns: TableOrSubquery, The returned table has the schema (ts TIMESTAMP, dur DURATION, partitions). \|ts\| is the start of the merged interval. \|dur\| is the duration of the merged interval. \|partitions\| is all of the columns in partition_columns. - -| Argument | Type | Description | -|---|---|---| -| intervals | TableOrSubquery | Table or subquery containing interval data. | -| partition_columns | ColumnNameList | Column name for partition grouping. | - -## Package: counters - -### counters.intervals - -#### Macros - -**counter_leading_intervals** - -For a given counter timeline (e.g. a single counter track), returns -intervals of time where the counter has the same value. For every run -of identical values, this macro will return a row for the first one, -the last one, and a row merging all other ones. This to to facilitate -construction of counters from delta_values. - -Intervals are computed in a "forward-looking" way. That is, if a counter -changes value at some timestamp, it's assumed it *just* reached that -value and it should continue to have that value until the next -value change. The final value is assumed to hold until the very end of -the trace. - -For example, suppose we have the following data: - -`ts=0, value=10, track_id=1 -ts=0, value=10, track_id=2 -ts=10, value=10, track_id=1 -ts=10, value=20, track_id=2 -ts=20, value=30, track_id=1 -[end of trace at ts = 40]` Then this macro will generate the following intervals: - -`ts=0, dur=10, value=10, track_id=1 -ts=10, dur=10, value=10, track_id=1 -ts=20, dur=10, value=30, track_id=1 -ts=0, dur=10, value=10, track_id=2 -ts=10, dur=30, value=20, track_id=2`Returns: TableOrSubquery, Table with the schema: id LONG, As passed in ts TIMESTAMP, As passed in dur DURATION, Difference to the timestamp for the leading row. track_id JOINID(track.id), As passed in value DOUBLE, As passed in next_value DOUBLE, Value for the leading row. delta_value DOUBLE Delta to the *lagging* row - note that this is not the same thing as (next_value - value). - -| Argument | Type | Description | -|---|---|---| -| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | - -## Package: linux - -### linux.block_io - -#### Views/Tables - -**linux_active_block_io_operations_by_device** - -VIEW -View tracking the number of IO operations remaining in the kernel IO queue or -a block device - -| Column | Type | Description | -|---|---|---| -| ts | LONG | timestamp when block_io_start or block_io_done happened | -| ops_in_queue_or_device | LONG | the number of IO operations in the kernel queue or the device | -| dev | LONG | the device processing the IO operations | +### time.conversion #### Functions -**linux_device_major_id** +
-Returns LONG: 12 bits major id +**time_from_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. | Argument | Type | Description | |---|---|---| -| dev | LONG | device id (userland dev_t value) | +| nanos | INT | Time duration in nanoseconds. | -**linux_device_minor_id** +
-Returns LONG: 20 bits minor id +
+ +
+ +**time_from_us** -\> INT. Converts a duration in microseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in microseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. | Argument | Type | Description | |---|---|---| -| dev | LONG | device id (userland dev_t value) | +| micros | INT | Time duration in microseconds. | + +
+ +
+ +
+ +**time_from_ms** -\> INT. Converts a duration in millseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in millseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| millis | INT | Time duration in milliseconds. | + +
+ +
+ +
+ +**time_from_s** -\> INT. Converts a duration in seconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in seconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| seconds | INT | Time duration in seconds. | + +
+ +
+ +
+ +**time_from_min** -\> INT. Converts a duration in minutes to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in minutes to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| minutes | INT | Time duration in minutes. | + +
+ +
+ +
+ +**time_from_hours** -\> INT. Converts a duration in hours to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in hours to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| hours | INT | Time duration in hours. | + +
+ +
+ +
+ +**time_from_days** -\> INT. Converts a duration in days to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in days to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| days | INT | Time duration in days. | + +
+ +
+ +
+ +**time_to_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_us** -\> INT. Converts a duration in nanoseconds to microseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to microseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in microseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_ms** -\> INT. Converts a duration in nanoseconds to millseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to millseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in milliseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_s** -\> INT. Converts a duration in nanoseconds to seconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to seconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in seconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_min** -\> INT. Converts a duration in nanoseconds to minutes + +
+ +
+ +
+ +Converts a duration in nanoseconds to minutes. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in minutes. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_hours** -\> INT. Converts a duration in nanoseconds to hours + +
+ +
+ +
+ +Converts a duration in nanoseconds to hours. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in hours. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_days** -\> INT. Converts a duration in nanoseconds to days + +
+ +
+ +
+ +Converts a duration in nanoseconds to days. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in days. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +## Package: linux ### linux.cpu.frequency #### Views/Tables -**cpu_frequency_counters** +
+ +**cpu_frequency_counters**. Counter information for each frequency change for each CPU + +
TABLE Counter information for each frequency change for each CPU. Finds each time @@ -5295,19 +1663,27 @@ region where a CPU frequency is constant. | Column | Type | Description | |---|---|---| -| id | LONG | Counter id. | -| track_id | JOINID(track.id) | Joinable with 'counter_track.id'. | -| ts | TIMESTAMP | Starting timestamp of the counter | -| dur | DURATION | Duration in which counter is constant and frequency doesn't change. | -| freq | LONG | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | -| ucpu | LONG | Unique CPU id. | -| cpu | LONG | CPU that corresponds to this counter. | +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter | +| dur | INT | Duration in which counter is constant and frequency doesn't change. | +| freq | INT | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | +| ucpu | INT | Unique CPU id. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
### linux.cpu.idle #### Views/Tables -**cpu_idle_counters** +
+ +**cpu_idle_counters**. Counter information for each idle state change for each CPU + +
TABLE Counter information for each idle state change for each CPU. Finds each time @@ -5315,220 +1691,252 @@ region where a CPU idle state is constant. | Column | Type | Description | |---|---|---| -| id | LONG | Counter id. | -| track_id | JOINID(track.id) | Joinable with 'counter_track.id'. | -| ts | TIMESTAMP | Starting timestamp of the counter. | -| dur | DURATION | Duration in which the counter is contant and idle state doesn't change. | -| idle | LONG | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | -| cpu | LONG | CPU that corresponds to this counter. | +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which the counter is contant and idle state doesn't change. | +| idle | INT | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
### linux.cpu.idle_stats #### Views/Tables -**cpu_idle_stats** +
+ +**cpu_idle_stats**. Aggregates cpu idle statistics per core. + +
TABLE -Aggregates cpu idle statistics per core. | Column | Type | Description | |---|---|---| -| cpu | LONG | CPU core number. | -| state | LONG | CPU idle state (C-states). | -| count | LONG | The count of entering idle state. | -| dur | DURATION | Total CPU core idle state duration. | -| avg_dur | DURATION | Average CPU core idle state duration. | -| idle_percent | DOUBLE | Idle state percentage of non suspend time (C-states + P-states). | +| cpu | INT | CPU core number. | +| state | INT | CPU idle state (C-states). | +| count | INT | The count of entering idle state. | +| dur | INT | Total CPU core idle state duration in nanoseconds. | +| avg_dur | INT | Average CPU core idle state duration in nanoseconds. | +| idle_percent | FLOAT | Idle state percentage of non suspend time (C-states + P-states). | + +
+ +
### linux.cpu.idle_time_in_state #### Views/Tables -**linux_per_cpu_idle_time_in_state_counters** +
+ +**cpu_idle_time_in_state_counters**. Counter information for sysfs cpuidle states. Tracks the percentage of time spent in each state between two timestamps, by dividing the incremental time spent in one state, by time all CPUS spent in any state. + +
TABLE -Percentage counter information for sysfs cpuidle states. -For each state per cpu, report the incremental time spent in one state, -divided by time spent in all states, between two timestamps. +Counter information for sysfs cpuidle states. +Tracks the percentage of time spent in each state between two timestamps, by +dividing the incremental time spent in one state, by time all CPUS spent in +any state. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp. | -| machine_id | LONG | The machine this residency is calculated for. | -| state | STRING | State name. | -| cpu | LONG | CPU. | -| idle_percentage | DOUBLE | Percentage of time this cpu spent in this state. | -| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | -| time_slice | LONG | Time this cpu spent in any state, in microseconds. | - -**linux_cpu_idle_time_in_state_counters** - -TABLE -Percentage counter information for sysfs cpuidle states. -For each state across all CPUs, report the incremental time spent in one -state, divided by time spent in all states, between two timestamps. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp. | -| machine_id | LONG | The machine this residency is calculated for. | -| state | STRING | State name. | +| ts | LONG | Timestamp. | +| state_name | STRING | State name. | | idle_percentage | DOUBLE | Percentage of time all CPUS spent in this state. | | total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | -| time_slice | LONG | Time all CPUS spent in any state, in microseconds. | +| time_slice | INT | Time all CPUS spent in any state, in microseconds. | + +
+ +
### linux.cpu.utilization.process #### Views/Tables -**cpu_cycles_per_process** +
+ +**cpu_cycles_per_process**. Aggregated CPU statistics for each process. + +
TABLE -Aggregated CPU statistics for each process. | Column | Type | Description | |---|---|---| -| upid | JOINID(process.id) | Unique process id | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | +| upid | INT | Unique process id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
#### Table Functions -**cpu_process_utilization_per_period** +
+ +**cpu_process_utilization_per_period** . Returns a table of process utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
Returns a table of process utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\|. For this reason first and last period might have lower then real utilization. - -| Argument | Type | Description | -|---|---|---| -| interval | LONG | Length of the period on which utilization should be averaged. | -| upid | JOINID(process.id) | Upid of the process. | +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +upid \| INT \| Upid of the process. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_process_utilization_per_second** +
+ +
+ +
+ +**cpu_process_utilization_per_second** . Returns a table of process utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
Returns a table of process utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\|. For this reason first and last period might have lower then real utilization. - -| Argument | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Upid of the process. | +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of the process. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_cycles_per_process_in_interval** +
-Aggregated CPU statistics for each process in a provided interval. +
-This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. +
+ +**cpu_cycles_per_process_in_interval** . Aggregated CPU statistics for each process in a provided interval. + +
+ +
+ +
| Argument | Type | Description | |---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | | Column | Type | Description | |---|---|---| -| upid | JOINID(process.id) | Unique process id. | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| awake_runtime | DURATION | Total runtime duration, while 'awake' (CPUs not suspended). | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | +| upid | INT | Unique process id. Joinable with `process.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | -**cpu_process_utilization_in_interval** +
-Returns a table with process utilization over a given interval. - -Utilization is computed as runtime over the duration of the interval, aggregated by UPID. -Utilization can be normalized (divide by number of cpus) or unnormalized. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Unique process id. | -| process_name | STRING | The name of the process. | -| awake_dur | LONG | Total runtime of all processes with this UPID, while 'awake' (CPUs not suspended). | -| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | -| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | +
### linux.cpu.utilization.slice #### Views/Tables -**cpu_cycles_per_thread_slice** +
+ +**cpu_cycles_per_thread_slice**. CPU cycles per each slice. + +
TABLE -CPU cycles per each slice. | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Id of a slice. | +| id | INT | Id of a slice. Alias of `slice.id`. | | name | STRING | Name of the slice. | -| utid | JOINID(thread.id) | Id of the thread the slice is running on. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | | thread_name | STRING | Name of the thread. | -| upid | JOINID(process.id) | Id of the process the slice is running on. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | | process_name | STRING | Name of the process. | -| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
#### Table Functions -**cpu_cycles_per_thread_slice_in_interval** +
-CPU cycles per each slice in interval. +**cpu_cycles_per_thread_slice_in_interval** . CPU cycles per each slice in interval. -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. +
+ +
+ +
| Argument | Type | Description | |---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | DURATION | Duration of the interval. | +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Thread slice. | +| id | INT | Id of a slice. Alias of `slice.id`. | | name | STRING | Name of the slice. | -| utid | JOINID(thread.id) | Thread the slice is running on. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | | thread_name | STRING | Name of the thread. | -| upid | JOINID(process.id) | Process the slice is running on. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | | process_name | STRING | Name of the process. | -| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
### linux.cpu.utilization.system #### Views/Tables -**cpu_utilization_per_second** +
+ +**cpu_utilization_per_second**. Table with system utilization per second. Utilization is calculated by sum of average utilization of each CPU every second + +
TABLE Table with system utilization per second. @@ -5538,283 +1946,271 @@ utilization. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_cycles** +
+ +
+ +
+ +**cpu_cycles**. Aggregated CPU statistics for whole trace + +
TABLE Aggregated CPU statistics for whole trace. Results in only one row. | Column | Type | Description | |---|---|---| -| millicycles | LONG | Sum of CPU millicycles. | -| megacycles | LONG | Sum of CPU megacycles. | -| runtime | DURATION | Total runtime of all threads running on all CPUs. | -| min_freq | LONG | Minimum CPU frequency in kHz. | -| max_freq | LONG | Maximum CPU frequency in kHz. | -| avg_freq | LONG | Average CPU frequency in kHz. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | -**cpu_cycles_per_cpu** +
+ +
+ +
+ +**cpu_cycles_per_cpu**. Aggregated CPU statistics for each CPU. + +
TABLE -Aggregated CPU statistics for each CPU. | Column | Type | Description | |---|---|---| -| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | -| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | -| millicycles | LONG | Sum of CPU millicycles. | -| megacycles | LONG | Sum of CPU megacycles. | -| runtime | DURATION | Total runtime of all threads running on CPU. | -| min_freq | LONG | Minimum CPU frequency in kHz. | -| max_freq | LONG | Maximum CPU frequency in kHz. | -| avg_freq | LONG | Average CPU frequency in kHz. | +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
#### Table Functions -**cpu_utilization_per_period** +
+ +**cpu_utilization_per_period** . Returns a table of system utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
Returns a table of system utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\|. For this reason first and last period might have lower then real utilization. - -| Argument | Type | Description | -|---|---|---| -| interval | LONG | Length of the period on which utilization should be averaged. | +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_cycles_in_interval** +
+ +
+ +
+ +**cpu_cycles_in_interval** . Aggregated CPU statistics in a provided interval + +
+ +
+ +
Aggregated CPU statistics in a provided interval. Results in one row. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Start of the interval. +dur \| INT \| Duration of the interval. | Column | Type | Description | |---|---|---| -| millicycles | LONG | Sum of CPU millicycles. | -| megacycles | LONG | Sum of CPU megacycles. | -| runtime | DURATION | Total runtime of all threads running on all CPUs. | -| awake_runtime | DURATION | Total runtime of all threads running on all CPUs, while 'awake' (CPUs not suspended). | -| min_freq | LONG | Minimum CPU frequency in kHz. | -| max_freq | LONG | Maximum CPU frequency in kHz. | -| avg_freq | LONG | Average CPU frequency in kHz. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | -**cpu_utilization_in_interval** +
-Returns a table of CPU utilization over a given interval. +
-Utilization is computed as runtime over the duration of the interval. -Utilization can be normalized (divide by number of cores) or unnormalized. +
-This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. +**cpu_cycles_per_cpu_in_interval** . Aggregated CPU statistics for each CPU in a provided interval. + +
+ +
+ +
| Argument | Type | Description | |---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | | Column | Type | Description | |---|---|---| -| awake_dur | LONG | Total runtime of all threads running on all CPUs, while 'awake' (CPUs not suspended). | -| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | -| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | CPU number. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | -**cpu_cycles_per_cpu_in_interval** +
-Aggregated CPU statistics for each CPU in a provided interval. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | -| cpu | LONG | CPU number. | -| millicycles | LONG | Sum of CPU millicycles. | -| megacycles | LONG | Sum of CPU megacycles. | -| runtime | DURATION | Total runtime of all threads running on CPU. | -| min_freq | LONG | Minimum CPU frequency in kHz. | -| max_freq | LONG | Maximum CPU frequency in kHz. | -| avg_freq | LONG | Average CPU frequency in kHz. | +
### linux.cpu.utilization.thread #### Views/Tables -**cpu_cycles_per_thread** +
+ +**cpu_cycles_per_thread**. Aggregated CPU statistics for each thread. + +
TABLE -Aggregated CPU statistics for each thread. | Column | Type | Description | |---|---|---| -| utid | JOINID(thread.id) | Thread | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | +| utid | INT | Unique thread id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
#### Table Functions -**cpu_thread_utilization_per_period** +
+ +**cpu_thread_utilization_per_period** . Returns a table of thread utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
Returns a table of thread utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\|. For this reason first and last period might have lower then real utilization. - -| Argument | Type | Description | -|---|---|---| -| interval | LONG | Length of the period on which utilization should be averaged. | -| utid | JOINID(thread.id) | Utid of the thread. | +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +utid \| INT \| Utid of the thread. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_thread_utilization_per_second** +
+ +
+ +
+ +**cpu_thread_utilization_per_second** . Returns a table of thread utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
Returns a table of thread utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\|. For this reason first and last period might have lower then real utilization. - -| Argument | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | Utid of the thread. | +Argument \| Type \| Description +--- \| --- \| --- +utid \| INT \| Utid of the thread. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp of start of a second. | +| ts | INT | Timestamp of start of a second. | | utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | | unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | -**cpu_cycles_per_thread_in_interval** +
-Aggregated CPU statistics for each thread in a provided interval. +
-This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. +
+ +**cpu_cycles_per_thread_in_interval** . Aggregated CPU statistics for each thread in a provided interval. + +
+ +
+ +
| Argument | Type | Description | |---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | | Column | Type | Description | |---|---|---| -| utid | JOINID(thread.id) | Thread with CPU cycles and frequency statistics. | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| awake_runtime | DURATION | Total runtime duration, while 'awake' (CPUs not suspended). | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | +| utid | INT | Unique thread id. Joinable with `thread.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | -**cpu_thread_utilization_in_interval** +
-Returns a table of thread utilization over a given interval. - -Utilization is computed as runtime over the duration of the interval, aggregated by UTID. -Utilization can be normalized (divide by number of CPUs) or unnormalized. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| upid | JOINID(process.id) | Unique process id. | -| utid | JOINID(thread.id) | Unique thread id. | -| thread_name | STRING | The name of the thread | -| awake_dur | LONG | Total runtime of all threads with this UTID, while 'awake' (CPUs not suspended). | -| awake_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, normalized by the number of CPUs. Values in \[0.0, 100.0\] | -| awake_unnormalized_utilization | DOUBLE | Percentage of 'awake_dur' over the 'awake' duration of the interval, unnormalized. Values in \[0.0, 100.0 \* \] | - -### linux.cpu.utilization.thread_cpu - -#### Views/Tables - -**cpu_cycles_per_thread_per_cpu** - -TABLE -Aggregated CPU statistics for each thread per CPU combination. -To operate properly this requires sched/sched_switch and power/cpu_frequency -ftrace events to be present in the trace. - -| Column | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | Thread | -| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | -| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | - -#### Table Functions - -**cpu_cycles_per_thread_per_cpu_in_interval** - -Aggregated CPU statistics for each thread per CPU combination in a provided interval. -To operate properly this requires sched/sched_switch and power/cpu_frequency -ftrace events to be present in the trace. -Warning: this query is expensive and might take a long time to execute when joined -with a large number of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Start of the interval. | -| dur | LONG | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | Thread with CPU cycles and frequency statistics. | -| ucpu | JOINID(cpu.id) | Unique CPU id. Joinable with `cpu.id`. | -| cpu | LONG | The number of the CPU. Might not be the same as ucpu in multi machine cases. | -| millicycles | LONG | Sum of CPU millicycles | -| megacycles | LONG | Sum of CPU megacycles | -| runtime | DURATION | Total runtime duration | -| min_freq | LONG | Minimum CPU frequency in kHz | -| max_freq | LONG | Maximum CPU frequency in kHz | -| avg_freq | LONG | Average CPU frequency in kHz | +
### linux.devfreq #### Views/Tables -**linux_devfreq_dsu_counter** +
+ +**linux_devfreq_dsu_counter**. ARM DSU device frequency counters + +
TABLE ARM DSU device frequency counters. This table will only be populated on @@ -5823,59 +2219,53 @@ and from ARM devices with the DSU (DynamIQ Shared Unit) hardware. | Column | Type | Description | |---|---|---| -| id | LONG | Unique identifier for this counter. | -| ts | TIMESTAMP | Starting timestamp of the counter. | -| dur | DURATION | Duration in which counter is constant and frequency doesn't chamge. | -| dsu_freq | LONG | Frequency in kHz of the device that corresponds to the counter. | +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| dsu_freq | INT | Frequency in kHz of the device that corresponds to the counter. | -### linux.irqs +
-#### Views/Tables +
-**linux_hard_irqs** +#### Table Functions -VIEW -All hard IRQs of the trace represented as slices. +
+ +**linux_get_devfreq_counters** . Gets devfreq frequency counter based on device queried + +
+ +
+ +
+ +Gets devfreq frequency counter based on device queried. These counters will +only be available if the "devfreq/devfreq_frequency" ftrace event is enabled. +Argument \| Type \| Description +--- \| --- \| --- +device_name \| STRING \| Devfreq name to query for. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Starting timestamp of this IRQ. | -| dur | DURATION | Duration of this IRQ. | -| name | STRING | The name of the IRQ. | -| id | JOINID(slice.id) | The id of the IRQ. | -| parent_id | JOINID(slice.id) | The id of this IRQ's parent IRQ (i.e. the IRQ that this IRQ preempted). | +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| freq | INT | Frequency in kHz of the device that corresponds to the counter. | -**linux_soft_irqs** +
-VIEW -All soft IRQs of the trace represented as slices. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Starting timestamp of this IRQ. | -| dur | DURATION | Duration of this IRQ. | -| name | STRING | The name of the IRQ. | -| id | JOINID(slice.id) | The id of the IRQ. | - -**linux_irqs** - -VIEW -All IRQs, including hard and soft IRQs, of the trace represented as slices. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Starting timestamp of this IRQ. | -| dur | DURATION | Duration of this IRQ. | -| name | STRING | The name of the IRQ. | -| id | JOINID(slice.id) | The id of the IRQ. | -| parent_id | JOINID(slice.id) | The id of this IRQ's parent IRQ (i.e. the IRQ that this IRQ preempted). | -| is_soft_irq | BOOL | Flag indicating if IRQ is soft IRQ | +
### linux.memory.high_watermark #### Views/Tables -**memory_rss_high_watermark_per_process** +
+ +**memory_rss_high_watermark_per_process**. For each process fetches the memory high watermark until or during timestamp. + +
VIEW For each process fetches the memory high watermark until or during @@ -5883,42 +2273,57 @@ timestamp. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| upid | JOINID(process.id) | Upid of the process | -| pid | LONG | Pid of the process | +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | | process_name | STRING | Name of the process | -| rss_high_watermark | LONG | Maximum `rss` value until now | +| rss_high_watermark | INT | Maximum `rss` value until now | + +
+ +
### linux.memory.process #### Views/Tables -**memory_rss_and_swap_per_process** +
+ +**memory_rss_and_swap_per_process**. Memory metrics timeline for each process. + +
VIEW -Memory metrics timeline for each process. | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestamp | -| dur | DURATION | Duration | -| upid | JOINID(process.id) | Upid of the process | -| pid | LONG | Pid of the process | +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | | process_name | STRING | Name of the process | -| anon_rss | LONG | Anon RSS counter value | -| file_rss | LONG | File RSS counter value | -| shmem_rss | LONG | Shared memory RSS counter value | -| rss | LONG | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | -| swap | LONG | Swap counter value | -| anon_rss_and_swap | LONG | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | -| rss_and_swap | LONG | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
### linux.perf.samples #### Views/Tables -**linux_perf_samples_summary_tree** +
+ +**linux_perf_samples_summary_tree** . Table summarising the callstacks captured during all perf samples in the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. + +
TABLE Table summarising the callstacks captured during all @@ -5932,35 +2337,43 @@ samples with the frame anywhere in the tree. | Column | Type | Description | |---|---|---| -| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | -| parent_id | LONG | The id of the parent callstack for this callstack. | +| id | INT | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | INT | The id of the parent callstack for this callstack. | | name | STRING | The function name of the frame for this callstack. | | mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | | source_file | STRING | The name of the file containing the function. | -| line_number | LONG | The line number in the file the function is located at. | -| self_count | LONG | The number of samples with this function as the leaf frame. | -| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
### linux.perf.spe #### Views/Tables -**linux_perf_spe_record** +
+ +**linux_perf_spe_record**. Contains ARM Statistical Profiling Extension records + +
VIEW | Column | Type | Description | |---|---|---| -| ts | TIMESTAMP | Timestap when the operation was sampled | -| utid | JOINID(thread.id) | Thread the operation executed in | +| ts | LONG | Timestap when the operation was sampled | +| utid | INT | Thread the operation executed in | | exception_level | STRING | Exception level the instruction was executed in | -| instruction_frame_id | LONG | Instruction virtual address | +| instruction_frame_id | INT | Instruction virtual address | | operation | STRING | Type of operation sampled | | data_virtual_address | LONG | The virtual address accessed by the operation (0 if no memory access was performed) | | data_physical_address | LONG | The physical address accessed by the operation (0 if no memory access was performed) | -| total_latency | LONG | Cycle count from the operation being dispatched for issue to the operation being complete. | -| issue_latency | LONG | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | -| translation_latency | LONG | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | +| total_latency | INT | Cycle count from the operation being dispatched for issue to the operation being complete. | +| issue_latency | INT | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | +| translation_latency | INT | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | | data_source | STRING | Where the data returned for a load operation was sourced | | exception_gen | BOOL | Operation generated an exception | | retired | BOOL | Operation architecturally retired | @@ -5983,11 +2396,19 @@ VIEW | recenty_fetched | BOOL | Wheter a load operation hit a recently fetched line in a cache | | data_snooped | BOOL | Whether a load operation snooped data from a cache outside the cache hierarchy of this core | +
+ +
+ ### linux.threads #### Views/Tables -**linux_kernel_threads** +
+ +**linux_kernel_threads**. All kernel threads of the trace + +
TABLE All kernel threads of the trace. As kernel threads are processes, provides @@ -5995,306 +2416,3909 @@ also process data. | Column | Type | Description | |---|---|---| -| upid | JOINID(process.id) | Upid of kernel thread. Alias of | -| utid | JOINID(thread.id) | Utid of kernel thread. Alias of | -| pid | LONG | Pid of kernel thread. Alias of | -| tid | LONG | Tid of kernel thread. Alias of | -| process_name | STRING | Name of kernel process. Alias of | -| thread_name | STRING | Name of kernel thread. Alias of | -| machine_id | LONG | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of | +| upid | INT | Upid of kernel thread. Alias of \|process.upid\|. | +| utid | INT | Utid of kernel thread. Alias of \|thread.utid\|. | +| pid | INT | Pid of kernel thread. Alias of \|process.pid\|. | +| tid | INT | Tid of kernel thread. Alias of \|process.pid\|. | +| process_name | STRING | Name of kernel process. Alias of \|process.name\|. | +| thread_name | STRING | Name of kernel thread. Alias of \|thread.name\|. | +| machine_id | INT | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of \|process.machine_id\|. | -## Package: pixel +
-### pixel.camera +
+ +## Package: android + +### android.anrs #### Views/Tables -**pixel_camera_frames** +
-TABLE -Break down camera Camera graph execution slices per node, port group, and frame. -This table extracts key identifiers from Camera graph execution slice names and -provides timing information for each processing stage. +**android_anrs**. List of all ANRs that occurred in the trace (one row per ANR). -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Unique identifier for this slice. | -| ts | TIMESTAMP | Start timestamp of the slice. | -| dur | DURATION | Duration of the slice execution. | -| track_id | JOINID(track.id) | Track ID for this slice. | -| utid | JOINID(thread.id) | Thread ID (utid) executing this slice. | -| thread_name | STRING | Name of the thread executing this slice. | -| node | STRING | Name of the processing node in the Camera graph. | -| port_group | STRING | Port group name for the node. | -| frame_number | LONG | Frame number being processed. | -| cam_id | LONG | Camera ID associated with this slice. | - -## Package: slices - -### slices.cpu_time - -#### Views/Tables - -**thread_slice_cpu_time** - -TABLE -Time each thread slice spent running on CPU. -Requires scheduling data to be available in the trace. - -| Column | Type | Description | -|---|---|---| -| id | JOINID(slice.id) | Slice. | -| name | STRING | Name of the slice. | -| utid | JOINID(thread.id) | Id of the thread the slice is running on. | -| thread_name | STRING | Name of the thread. | -| upid | JOINID(process.id) | Id of the process the slice is running on. | -| process_name | STRING | Name of the process. | -| cpu_time | LONG | Duration of the time the slice was running. | - -**thread_slice_cpu_cycles** +
VIEW -CPU cycles per each slice. | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Id of a slice. | -| name | STRING | Name of the slice. | -| utid | JOINID(thread.id) | Id of the thread the slice is running on. | -| thread_name | STRING | Name of the thread. | -| upid | JOINID(process.id) | Id of the process the slice is running on. | -| process_name | STRING | Name of the process. | -| millicycles | LONG | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | LONG | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| process_name | STRING | Name of the process that triggered the ANR. | +| pid | INT | PID of the process that triggered the ANR. | +| upid | INT | UPID of the process that triggered the ANR. | +| error_id | STRING | UUID of the ANR (generated on the platform). | +| ts | INT | Timestamp of the ANR. | +| subject | STRING | Subject line of the ANR. | -### slices.self_dur +
+ +
+ +### android.app_process_starts #### Views/Tables -**slice_self_dur** +
+ +**android_app_process_starts**. All app cold starts with information about their cold start reason: broadcast, service, activity or provider. + +
TABLE -For every slice in the `slice` table, computes the "self-duration": the time -spent in the slice but *not* spent in any child slices. +All app cold starts with information about their cold start reason: +broadcast, service, activity or provider. | Column | Type | Description | |---|---|---| -| id | ID(slice.id) | The id of the slice. | -| self_dur | DURATION | The self duration for the slice: the time spent in the slice but not any child slices. | +| start_id | INT | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | +| id | INT | Slice id of intent received in the app. | +| track_id | INT | Track id of the intent received in the app. | +| process_name | STRING | Name of the process receiving the intent. | +| pid | INT | Pid of the process receiving the intent. | +| upid | INT | Upid of the process receiving the intent. | +| intent | STRING | Intent action or component responsible for the cold start. | +| reason | STRING | Process start reason: activity, broadcast, service or provider. | +| proc_start_ts | INT | Timestamp the process start was dispatched from system_server. | +| proc_start_dur | INT | Duration to dispatch the process start from system_server. | +| bind_app_ts | INT | Timestamp the bindApplication started in the app. | +| bind_app_dur | INT | Duration to complete bindApplication in the app. | +| intent_ts | INT | Timestamp the Intent was received in the app. | +| intent_dur | INT | Duration to handle intent in the app. | +| total_dur | INT | Total duration from proc_start dispatched to intent completed. | -### slices.stack +
+ +
+ +### android.auto.multiuser #### Views/Tables -**slice_with_stack_id** +
-VIEW -View that provides stack_id and parent_stack_id for all slices by computing -them on-demand. +**android_auto_multiuser_timing**. Time elapsed between the latest user start and the specific end event like package startup(ex carlauncher) or previous user stop. -> [!NOTE] -> **Note:** This view computes stack hashes on-demand, which may be slower than the previous C++ implementation. +
+ +TABLE +Time elapsed between the latest user start +and the specific end event +like package startup(ex carlauncher) or previous user stop. | Column | Type | Description | |---|---|---| -| id | ID(slice.id) | Slice id. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | -| thread_dur | LONG | Alias of `slice.thread_dur`. | -| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | -| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | -| stack_id | LONG | A unique identifier obtained from the names and categories of all slices in this stack. Computed on-demand. | -| parent_stack_id | LONG | The stack_id for the parent of this slice. 0 if there is no parent. | +| event_start_user_id | STRING | Id of the started android user | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | + +
+ +
+ +
+ +**android_auto_multiuser_timing_with_previous_user_resource_usage** . This table extends `android_auto_multiuser_timing` table with previous user resource usage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Start user id | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | +| user_id | INT | User id | +| total_cpu_time | LONG | Total CPU time for a user | +| total_memory_usage_kb | LONG | Total memory user for a user | + +
+ +
+ +### android.battery + +#### Views/Tables + +
+ +**android_battery_charge**. Battery charge at timestamp. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp. | +| current_avg_ua | DOUBLE | Current average micro ampers. | +| capacity_percent | DOUBLE | Current capacity percentage. | +| charge_uah | DOUBLE | Current charge in micro ampers. | +| current_ua | DOUBLE | Current micro ampers. | + +
+ +
+ +### android.battery_stats + +#### Views/Tables + +
+ +**android_battery_stats_state**. View of human readable battery stats counter-based states + +
+ +VIEW +View of human readable battery stats counter-based states. These are recorded +by BatteryStats as a bitmap where each 'category' has a unique value at any +given time. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| value | INT | The counter value as a number. | +| value_name | STRING | The counter value as a human-readable string. | + +
+ +
+ +
+ +**android_battery_stats_event_slices**. View of slices derived from battery_stats events + +
+ +VIEW +View of slices derived from battery_stats events. Battery stats records all +events as instants, however some may indicate whether something started or +stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground +apps or long wakes include these details and allow drawing slices between +instant events found in a trace. + +For example, we may see an event like the following on 'battery_stats.top': + + -top=10215:"com.google.android.apps.nexuslauncher" + +This view will find the associated start ('+top') with the matching suffix +(everything after the '=') to construct a slice. It computes the timestamp +and duration from the events and extract the details as follows: + + track_name='battery_stats.top' + str_value='com.google.android.apps.nexuslauncher' + int_value=10215 + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| str_value | STRING | String value. | +| int_value | INT | Int value. | + +
+ +
+ +#### Functions + +
+ +**android_battery_stats_counter_to_string** -\> STRING. Converts a battery_stats counter value to human readable string. + +
+ +
+ +
+ +Returns STRING: The human-readable name for the counter value. + +| Argument | Type | Description | +|---|---|---| +| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | +| value | FLOAT | The counter value. | + +
+ +
+ +### android.binder + +#### Views/Tables + +
+ +**android_binder_metrics_by_process**. Count Binder transactions per process. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that started the binder transaction. | +| pid | INT | PID of the process that started the binder transaction. | +| slice_name | STRING | Name of the slice with binder transaction. | +| event_count | INT | Number of binder transactions in process in slice. | + +
+ +
+ +
+ +**android_sync_binder_thread_state_by_txn**. Aggregated thread_states on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the thread state durations. The \|thread_state_type\| column represents whether a given 'aggregated thread_state' row is on the client or server side + +
+ +VIEW +Aggregated thread_states on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the thread state durations. +The \|thread_state_type\| column represents whether a given 'aggregated thread_state' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client timestamp | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server timestamp | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| thread_state | STRING | a thread_state that occurred in the txn | +| thread_state_dur | INT | aggregated dur of the \|thread_state\| in the txn | +| thread_state_count | INT | aggregated count of the \|thread_state\| in the txn | + +
+ +
+ +
+ +**android_sync_binder_blocked_functions_by_txn**. Aggregated blocked_functions on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the kernel blocked function durations. The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' row is on the client or server side + +
+ +VIEW +Aggregated blocked_functions on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the kernel blocked function durations. +The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client ts | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server ts | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| blocked_function | STRING | blocked kernel function in a thread state | +| blocked_function_dur | INT | aggregated dur of the \|blocked_function\| in the txn | +| blocked_function_count | INT | aggregated count of the \|blocked_function\| in the txn | + +
+ +
+ +
+ +**android_binder_txns**. Breakdown binder transactions per txn. It returns data about the client and server ends of every binder transaction async. + +
+ +TABLE +Breakdown binder transactions per txn. +It returns data about the client and server ends of every binder transaction async. + +| Column | Type | Description | +|---|---|---| +| aidl_name | STRING | name of the binder interface if existing. | +| aidl_ts | INT | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | +| aidl_dur | INT | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | +| binder_txn_id | INT | slice id of the binder txn. | +| client_process | STRING | name of the client process. | +| client_thread | STRING | name of the client thread. | +| client_upid | INT | Upid of the client process. | +| client_utid | INT | Utid of the client thread. | +| client_tid | INT | Tid of the client thread. | +| client_pid | INT | Pid of the client thread. | +| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | +| client_ts | INT | timestamp of the client txn. | +| client_dur | INT | wall clock dur of the client txn. | +| binder_reply_id | INT | slice id of the binder reply. | +| server_process | STRING | name of the server process. | +| server_thread | STRING | name of the server thread. | +| server_upid | INT | Upid of the server process. | +| server_utid | INT | Utid of the server thread. | +| server_tid | INT | Tid of the server thread. | +| server_pid | INT | Pid of the server thread. | +| server_ts | INT | timestamp of the server txn. | +| server_dur | INT | wall clock dur of the server txn. | +| client_oom_score | INT | oom score of the client process at the start of the txn. | +| server_oom_score | INT | oom score of the server process at the start of the reply. | +| is_sync | BOOL | whether the txn is synchronous or async (oneway). | +| client_monotonic_dur | INT | monotonic clock dur of the client txn. | +| server_monotonic_dur | INT | monotonic clock dur of the server txn. | +| client_package_version_code | INT | Client package version_code. | +| server_package_version_code | INT | Server package version_code. | +| is_client_package_debuggable | INT | Whether client package is debuggable. | +| is_server_package_debuggable | INT | Whether server package is debuggable. | + +
+ +
#### Table Functions -**ancestor_slice_by_stack** +
-Returns all slices that have the given stack_id, along with their ancestors. +**android_binder_outgoing_graph** . Returns a DAG of all outgoing binder txns from a process. The roots of the graph are the threads making the txns and the graph flows from: thread -\> server_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. -The stack_id can be obtained from the slice_with_stack_id view. +
-| Argument | Type | Description | -|---|---|---| -| stack_hash | LONG | The stack hash to search for. | +
+ +
+ +Returns a DAG of all outgoing binder txns from a process. +The roots of the graph are the threads making the txns and the graph flows from: +thread -\> server_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an outgoing graph for. | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Slice id. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | -| thread_dur | LONG | Alias of `slice.thread_dur`. | +| pprof | BYTES | Pprof of outgoing binder txns. | -**descendant_slice_by_stack** +
-Returns all slices that have the given stack_id, along with their descendants. +
-The stack_id can be obtained from the slice_with_stack_id view. +
-| Argument | Type | Description | -|---|---|---| -| stack_hash | LONG | The stack hash to search for. | +**android_binder_incoming_graph** . Returns a DAG of all incoming binder txns from a process. The roots of the graph are the clients making the txns and the graph flows from: client_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a DAG of all incoming binder txns from a process. +The roots of the graph are the clients making the txns and the graph flows from: +client_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an incoming graph for. | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Slice id. | -| ts | TIMESTAMP | Alias of `slice.ts`. | -| dur | DURATION | Alias of `slice.dur`. | -| track_id | JOINID(track.id) | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | LONG | Alias of `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias of `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias of `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias of `slice.thread_ts`. | -| thread_dur | LONG | Alias of `slice.thread_dur`. | +| pprof | BYTES | Pprof of incoming binder txns. | -### slices.time_in_state +
+ +
+ +
+ +**android_binder_graph** . Returns a graph of all binder txns in a trace. The nodes are client_process and server_process. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a graph of all binder txns in a trace. +The nodes are client_process and server_process. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +min_client_oom_score \| INT \| Matches txns from client_processes greater than or equal to the OOM score. +max_client_oom_score \| INT \| Matches txns from client_processes less than or equal to the OOM score. +min_server_oom_score \| INT \| Matches txns to server_processes greater than or equal to the OOM score. +max_server_oom_score \| INT \| Matches txns to server_processes less than or equal to the OOM score. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of binder txns. | + +
+ +
+ +### android.binder_breakdown #### Views/Tables -**thread_slice_time_in_state** +
+ +**android_binder_server_breakdown**. Server side binder breakdowns per transactions per txn. + +
TABLE -For each thread slice, returns the sum of the time it spent in various -scheduling states. - -Requires scheduling data to be available in the trace. | Column | Type | Description | |---|---|---| -| id | JOINID(slice.id) | Thread slice. | -| name | STRING | Name of the slice. | -| utid | JOINID(thread.id) | Thread the slice is running on. | +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder reply with a single reason. | +| dur | INT | Duration of an exclusive interval during the binder reply with a single reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | + +
+ +
+ +
+ +**android_binder_client_breakdown**. Client side binder breakdowns per transactions per txn. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder txn with a single latency reason. | +| dur | INT | Duration of an exclusive interval during the binder txn with a single latency reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | + +
+ +
+ +### android.cpu.cluster_type + +#### Views/Tables + +
+ +**android_cpu_cluster_mapping**. Stores the mapping of a cpu to its cluster type - e.g + +
+ +TABLE +Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. +This cluster type is determined by initially using cpu_capacity from sysfs +and grouping clusters with identical capacities, ordered by size. +In the case that capacities are not present, max frequency is used instead. +If nothing is avaiable, NULL is returned. + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Alias of `cpu.ucpu`. | +| cpu | INT | Alias of `cpu.cpu`. | +| cluster_type | STRING | The cluster type of the CPU. | + +
+ +
+ +### android.device + +#### Views/Tables + +
+ +**android_device_name**. Extract name of the device based on metadata from the trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Device name. | + +
+ +
+ +### android.dvfs + +#### Views/Tables + +
+ +**android_dvfs_counters**. Dvfs counter with duration. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| ts | INT | Timestamp when counter value changed. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | + +
+ +
+ +
+ +**android_dvfs_counter_stats**. Aggregates dvfs counter slice for statistic. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name on which all the other values are aggregated on. | +| max | DOUBLE | Max of all counter values for the counter name. | +| min | DOUBLE | Min of all counter values for the counter name. | +| dur | INT | Duration between the first and last counter value for the counter name. | +| wgt_avg | FLOAT | Weighted avergate of all the counter values for the counter name. | + +
+ +
+ +
+ +**android_dvfs_counter_residency**. Aggregates dvfs counter slice for residency + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | +| pct | DOUBLE | Counter duration as a percentage of total duration. | + +
+ +
+ +### android.frames.jank_type + +#### Functions + +
+ +**android_is_sf_jank_type** -\> BOOL. Categorizes whether the jank was caused by Surface Flinger + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents sf jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +
+ +**android_is_app_jank_type** -\> BOOL. Categorizes whether the jank was caused by the app + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents app jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +### android.frames.per_frame_metrics + +#### Views/Tables + +
+ +**android_frames_overrun**. The amount by which each frame missed of hit its deadline + +
+ +TABLE +The amount by which each frame missed of hit its deadline. Negative if the +deadline was not missed. Frames are considered janky if `overrun` is +positive. +Calculated as the difference between the end of the +`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the +frame. +Availability: from S (API 31). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | + +
+ +
+ +
+ +**android_frames_ui_time**. How much time did the frame's Choreographer callbacks take. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| ui_time | INT | UI time duration | + +
+ +
+ +
+ +**android_app_vsync_delay_per_frame**. App Vsync delay for a frame + +
+ +TABLE +App Vsync delay for a frame. The time between the VSYNC-app signal and the +start of Choreographer work. +Calculated as time difference between the actual frame start (from +`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` +slice. +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | App VSYNC delay. | + +
+ +
+ +
+ +**android_cpu_time_per_frame** . How much time did the frame take across the UI Thread + RenderThread. Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice duration and summed durations of all `DrawFrame` slices associated with this frame. Availability: from N (API 24). For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +How much time did the frame take across the UI Thread + RenderThread. +Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice +duration and summed durations of all `DrawFrame` slices associated with this +frame. +Availability: from N (API 24). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | +| do_frame_dur | INT | Duration of `Choreographer#doFrame` slice. | +| draw_frame_dur | INT | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | +| cpu_time | INT | CPU time across the UI Thread + RenderThread. | + +
+ +
+ +
+ +**android_frame_stats**. Aggregated stats of the frame. For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +Aggregated stats of the frame. + +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | +| cpu_time | INT | How much time did the frame take across the UI Thread + RenderThread. | +| ui_time | INT | How much time did the frame's Choreographer callbacks take. | +| was_jank | BOOL | Was frame janky. | +| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | +| was_big_jank | BOOL | CPU time of the frame took over 50ms. | +| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | + +
+ +
+ +### android.frames.timeline + +#### Views/Tables + +
+ +**android_frames_choreographer_do_frame** . All of the `Choreographer#doFrame` slices with their frame id. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| ui_thread_utid | INT | Utid of the UI thread | +| upid | INT | Upid of application process | +| ts | INT | Timestamp of the slice. | + +
+ +
+ +
+ +**android_frames_draw_frame** . All of the `DrawFrame` slices with their frame id and render thread. There might be multiple DrawFrames slices for a single vsync (frame id). This happens when we are drawing multiple layers (e.g + +
+ +TABLE +All of the `DrawFrame` slices with their frame id and render thread. +There might be multiple DrawFrames slices for a single vsync (frame id). +This happens when we are drawing multiple layers (e.g. status bar and +notifications). + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| render_thread_utid | INT | Utid of the render thread | +| upid | INT | Upid of application process | + +
+ +
+ +
+ +**android_frames**. All slices related to one frame + +
+ +TABLE +All slices related to one frame. Aggregates `Choreographer#doFrame`, +`DrawFrame`, `actual_frame_timeline_slice` and +`expected_frame_timeline_slice` slices. +See https://perfetto.dev/docs/data-sources/frametimeline for details. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | +| dur | INT | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | +| actual_frame_timeline_count | INT | Count of slices in `actual_frame_timeline_slice` related to this frame. | +| expected_frame_timeline_count | INT | Count of slices in `expected_frame_timeline_slice` related to this frame. | + +
+ +
+ +#### Table Functions + +
+ +**android_first_frame_after** . Returns first frame after the provided timestamp + +
+ +
+ +
+ +Returns first frame after the provided timestamp. The returning table has at +most one row. +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Timestamp. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | +| dur | INT | Duration of the frame. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | + +
+ +
+ +### android.freezer + +#### Views/Tables + +
+ +**android_freezer_events**. All frozen processes and their frozen duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Upid of frozen process | +| pid | INT | Pid of frozen process | +| ts | INT | Timestamp process was frozen. | +| dur | INT | Duration process was frozen for. | +| unfreeze_reason_int | INT | Unfreeze reason Integer. | +| unfreeze_reason_str | STRING | Unfreeze reason String. | + +
+ +
+ +### android.garbage_collection + +#### Views/Tables + +
+ +**android_garbage_collection_events**. All Garbage collection events with a breakdown of the time spent and heap reclaimed. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| tid | INT | Tid of thread running garbage collection. | +| pid | INT | Pid of process running garbage collection. | +| utid | INT | Utid of thread running garbage collection. | +| upid | INT | Upid of process running garbage collection. | +| thread_name | STRING | Name of thread running garbage collection. | +| process_name | STRING | Name of process running garbage collection. | +| gc_type | STRING | Type of garbage collection. | +| is_mark_compact | INT | Whether gargage collection is mark compact or copying. | +| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | +| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | +| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | +| gc_id | INT | Garbage collection id. | +| gc_ts | INT | Garbage collection timestamp. | +| gc_dur | INT | Garbage collection wall duration. | +| gc_running_dur | INT | Garbage collection duration spent executing on CPU. | +| gc_runnable_dur | INT | Garbage collection duration spent waiting for CPU. | +| gc_unint_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel on IO. | +| gc_unint_non_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel without IO. | +| gc_int_dur | INT | Garbage collection duration spent waiting in interruptible sleep. | + +
+ +
+ +### android.gpu.frequency + +#### Views/Tables + +
+ +**android_gpu_frequency**. GPU frequency counter per GPU. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| gpu_id | INT | GPU id. Joinable with `gpu_counter_track.gpu_id`. | +| gpu_freq | INT | GPU frequency | + +
+ +
+ +### android.gpu.memory + +#### Views/Tables + +
+ +**android_gpu_memory_per_process**. Counter for GPU memory per process with duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| gpu_memory | INT | GPU memory | + +
+ +
+ +### android.input + +#### Views/Tables + +
+ +**android_input_events**. All input events with round trip latency breakdown + +
+ +TABLE +All input events with round trip latency breakdown. Input delivery is socket based and every +input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure +latencies between: +1. Input dispatch event sent from OS. +2. Input dispatch event received in app. +3. Input ACK event sent from app. +4. Input ACk event received in OS. + +| Column | Type | Description | +|---|---|---| +| dispatch_latency_dur | INT | Duration from input dispatch to input received. | +| handling_latency_dur | INT | Duration from input received to input ACK sent. | +| ack_latency_dur | INT | Duration from input ACK sent to input ACK recieved. | +| total_latency_dur | INT | Duration from input dispatch to input event ACK received. | +| end_to_end_latency_dur | INT | Duration from input read to frame present time. Null if an input event has no associated frame event. | +| tid | INT | Tid of thread receiving the input event. | +| thread_name | STRING | Name of thread receiving the input event. | +| pid | INT | Pid of process receiving the input event. | +| process_name | STRING | Name of process receiving the input event. | +| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | +| event_action | STRING | Input event action. | +| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | +| event_channel | STRING | Input event channel name. | +| input_event_id | STRING | Unique identifier for the input event. | +| read_time | INT | Timestamp input event was read by InputReader. | +| dispatch_track_id | INT | Thread track id of input event dispatching thread. | +| dispatch_ts | INT | Timestamp input event was dispatched. | +| dispatch_dur | INT | Duration of input event dispatch. | +| receive_track_id | INT | Thread track id of input event receiving thread. | +| receive_ts | INT | Timestamp input event was received. | +| receive_dur | INT | Duration of input event receipt. | +| frame_id | INT | Vsync Id associated with the input. Null if an input event has no associated frame event. | + +
+ +
+ +
+ +**android_key_events**. Key events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_motion_events**. Motion events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_input_event_dispatch**. Input event dispatching information in Android (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | Event ID of the input event that was dispatched | +| arg_set_id | INT | Extra args parsed from the proto message | +| vsync_id | INT | Vsync ID that identifies the state of the windows during which the dispatch decision was made | +| window_id | INT | Window ID of the window receiving the event | + +
+ +
+ +### android.job_scheduler + +#### Views/Tables + +
+ +**android_job_scheduler_events**. All scheduled jobs and their latencies. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| job_id | INT | Id of the scheduled job assigned by the app developer. | +| uid | INT | Uid of the process running the scheduled job. | +| package_name | STRING | Package name of the process running the scheduled job. | +| job_service_name | STRING | Service component name of the scheduled job. | +| track_id | INT | Thread track id of the job scheduler event slice. | +| id | INT | Slice id of the job scheduler event slice. | +| ts | INT | Timestamp the job was scheduled. | +| dur | INT | Duration of the scheduled job. | + +
+ +
+ +### android.memory.dmabuf + +#### Views/Tables + +
+ +**android_dmabuf_allocs**. Track dmabuf allocations, re-attributing gralloc allocations to their source (if binder transactions to gralloc are recorded). + +
+ +TABLE +Track dmabuf allocations, re-attributing gralloc allocations to their source +(if binder transactions to gralloc are recorded). + +| Column | Type | Description | +|---|---|---| +| ts | INT | timestamp of the allocation | +| buf_size | INT | allocation size (will be negative for release) | +| inode | INT | dmabuf inode | +| utid | INT | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | +| tid | INT | tid of thread responsible for the allocation | +| thread_name | STRING | thread name | +| upid | INT | upid of process responsible for the allocation (matches utid) | +| pid | INT | pid of process responsible for the allocation | +| process_name | STRING | process name | + +
+ +
+ +### android.memory.heap_graph.dominator_tree + +#### Views/Tables + +
+ +**heap_graph_dominator_tree**. All reachable heap graph objects, their immediate dominators and summary of their dominated sets. The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. Each reachable object is a node in the dominator tree, their immediate dominator is their parent node in the tree, and their dominated set is all their descendants in the tree + +
+ +TABLE +All reachable heap graph objects, their immediate dominators and summary of +their dominated sets. +The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. +Each reachable object is a node in the dominator tree, their immediate +dominator is their parent node in the tree, and their dominated set is all +their descendants in the tree. All size information come from the +heap_graph_object prelude table. + +| Column | Type | Description | +|---|---|---| +| id | INT | Heap graph object id. | +| idom_id | INT | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | +| dominated_obj_count | INT | Count of all objects dominated by this object, self inclusive. | +| dominated_size_bytes | INT | Total self_size of all objects dominated by this object, self inclusive. | +| dominated_native_size_bytes | INT | Total native_size of all objects dominated by this object, self inclusive. | +| depth | INT | Depth of the object in the dominator tree. Depth of root objects are 1. | + +
+ +
+ +### android.memory.heap_graph.heap_graph_class_aggregation + +#### Views/Tables + +
+ +**android_heap_graph_class_aggregation**. Class-level breakdown of the java heap. Per type name aggregates the object stats and the dominator tree stats. + +
+ +TABLE +Class-level breakdown of the java heap. +Per type name aggregates the object stats and the dominator tree stats. + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid | +| graph_sample_ts | INT | Heap dump timestamp | +| type_id | INT | Class type id | +| type_name | STRING | Class name (deobfuscated if available) | +| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | +| obj_count | INT | Count of class instances | +| size_bytes | INT | Size of class instances | +| native_size_bytes | INT | Native size of class instances | +| reachable_obj_count | INT | Count of reachable class instances | +| reachable_size_bytes | INT | Size of reachable class instances | +| reachable_native_size_bytes | INT | Native size of reachable class instances | +| dominated_obj_count | INT | Count of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_size_bytes | INT | Size of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_native_size_bytes | INT | Native size of all objects dominated by instances of this class Only applies to reachable objects | + +
+ +
+ +### android.memory.process + +#### Views/Tables + +
+ +**memory_oom_score_with_rss_and_swap_per_process**. Process memory and it's OOM adjuster scores + +
+ +TABLE +Process memory and it's OOM adjuster scores. Detects transitions, each new +interval means that either the memory or OOM adjuster score of the process changed. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score or memory of the process changed | +| dur | INT | Duration until the next oom_adj score or memory change of the process. | +| score | INT | oom adjuster score of the process. | +| bucket | STRING | oom adjuster bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| pid | INT | Pid of the process having an oom_adj update. | +| oom_adj_id | INT | Slice of the latest oom_adj update in the system_server. Alias of `slice.id`. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
+ +### android.monitor_contention + +#### Views/Tables + +
+ +**android_monitor_contention**. Contains parsed monitor contention slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain**. Contains parsed monitor contention slices with the parent-child relationships. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| parent_id | INT | Id of monitor contention slice blocking this contention. | +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | +| child_id | INT | Id of monitor contention slice blocked by this contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state**. Note that we only span join the duration where the lock was actually held and contended. This can be less than the duration the lock was 'waited on' when a different waiter acquired the lock earlier than the first waiter. + +
+ +TABLE +Note that we only span join the duration where the lock was actually held and contended. +This can be less than the duration the lock was 'waited on' when a different waiter acquired the +lock earlier than the first waiter. + +| Column | Type | Description | +|---|---|---| +| parent_id | None | Id of slice blocking the blocking_thread. | +| blocking_method | None | Name of the method holding the lock. | +| blocked_methhod | None | Name of the method trying to acquire the lock. | +| short_blocking_method | None | Blocking_method without arguments and return types. | +| short_blocked_method | None | Blocked_method without arguments and return types. | +| blocking_src | None | File location of blocking_method in form . | +| blocked_src | None | File location of blocked_method in form . | +| waiter_count | None | Zero indexed number of threads trying to acquire the lock. | +| blocking_utid | None | Utid of the blocking \|thread_state\|. | +| blocking_thread_name | None | Thread name of thread holding the lock. | +| upid | None | Upid of process experiencing lock contention. | +| process_name | None | Process name of process experiencing lock contention. | +| id | None | Slice id of lock contention. | +| ts | None | Timestamp of the blocking \|thread_state\|. | +| dur | None | Wall clock duration of lock contention. | +| monotonic_dur | None | Monotonic clock duration of lock contention. | +| track_id | None | Thread track id of blocked thread. | +| is_blocked_main_thread | None | Whether the blocked thread is the main thread. | +| is_blocking_main_thread | None | Whether the blocking thread is the main thread. | +| binder_reply_id | None | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | None | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | None | Tid of binder reply slice if lock contention was part of a binder txn. | +| state | None | Thread state of the blocking thread. | +| blocked_function | None | Blocked kernel function of the blocking thread. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state_by_txn**. Aggregated thread_states on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention slice, it returns the aggregated sum of all the thread states on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated thread_states on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention slice, it returns the aggregated sum of all the thread states on the +blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| thread_state | STRING | A \|thread_state\| that occurred in the blocking thread during the contention. | +| thread_state_dur | INT | Total time the blocking thread spent in the \|thread_state\| during contention. | +| thread_state_count | INT | Count of all times the blocking thread entered \|thread_state\| during the contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_blocked_functions_by_txn**. Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention, it returns the aggregated sum of all the kernel blocked function durations on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention, it returns the aggregated sum of all the kernel +blocked function durations on the blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | +| blocked_function_dur | INT | Total time the blocking thread spent in the \|blocked_function\| during the contention. | +| blocked_function_count | INT | Count of all times the blocking thread executed the \|blocked_function\| during the contention. | + +
+ +
+ +#### Functions + +
+ +**android_extract_android_monitor_contention_blocking_thread** -\> STRING. Extracts the blocking thread from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_tid** -\> INT. Extracts the blocking thread tid from a slice name + +
+ +
+ +
+ +Returns INT: Blocking thread tid + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_method** -\> STRING. Extracts the blocking method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocking_method** -\> STRING. Extracts a shortened form of the blocking method name from a slice name. The shortened form discards the parameter and return types. + +
+ +
+ +
+ +Extracts a shortened form of the blocking method name from a slice name. +The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_method** -\> STRING. Extracts the monitor contention blocked method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocked_method** -\> STRING. Extracts a shortened form of the monitor contention blocked method name from a slice name + +
+ +
+ +
+ +Extracts a shortened form of the monitor contention blocked method name +from a slice name. The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_waiter_count** -\> INT. Extracts the number of waiters on the monitor from a slice name + +
+ +
+ +
+ +Returns INT: Count of waiters on the lock + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_src** -\> STRING. Extracts the monitor contention blocking source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_src** -\> STRING. Extracts the monitor contention blocked source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +#### Table Functions + +
+ +**android_monitor_contention_graph** . Returns a DAG of all Java lock contentions in a process. Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. + +
+ +
+ +
+ +Returns a DAG of all Java lock contentions in a process. +Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. Argument \| Type \| Description --- \| --- \| --- upid \| INT \| Upid of process to generate a lock graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of lock graph. | + +
+ +
+ +### android.network_packets + +#### Views/Tables + +
+ +**android_network_packets**. Android network packet events (from android.network_packets data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | Duration (non-zero only in aggregate events) | +| track_name | STRING | The track name (interface and direction) | +| package_name | STRING | Traffic package source (or uid=$X if not found) | +| iface | STRING | Traffic interface name (linux interface name) | +| direction | STRING | Traffic direction ('Transmitted' or 'Received') | +| packet_count | INT | Number of packets in this event | +| packet_length | INT | Number of bytes in this event (wire size) | +| packet_transport | STRING | Transport used for traffic in this event | +| packet_tcp_flags | INT | TCP flags used by tcp frames in this event | +| socket_tag | STRING | The Android traffic tag of the network socket | +| socket_uid | INT | The Linux user id of the network socket | +| local_port | INT | The local port number (for udp or tcp only) | +| remote_port | INT | The remote port number (for udp or tcp only) | +| packet_icmp_type | INT | 1-byte ICMP type identifier. | +| packet_icmp_code | INT | 1-byte ICMP code identifier. | +| packet_tcp_flags_int | INT | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | +| socket_tag_int | INT | Packet's socket tag as an integer. | + +
+ +
+ +### android.oom_adjuster + +#### Views/Tables + +
+ +**android_oom_adj_intervals**. All oom adj state intervals across all processes along with the reason for the state update. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score of the process changed | +| dur | INT | Duration until the next oom_adj score change of the process. | +| score | INT | oom_adj score of the process. | +| bucket | STRING | oom_adj bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| oom_adj_id | INT | Slice id of the latest oom_adj update in the system_server. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track id of the latest oom_adj update in the system_server | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | + +
+ +
+ +#### Functions + +
+ +**android_oom_adj_score_to_bucket_name** -\> STRING. Converts an oom_adj score Integer to String sample name. One of: cached, background, job, foreground_service, bfgs, foreground and system. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String sample name. +One of: cached, background, job, foreground_service, bfgs, foreground and +system. +Returns STRING: Returns the sample bucket based on the oom score. + +| Argument | Type | Description | +|---|---|---| +| oom_score | INT | `oom_score` value | + +
+ +
+ +
+ +**android_oom_adj_score_to_detailed_bucket_name** -\> STRING. Converts an oom_adj score Integer to String bucket name. Deprecated: use `android_oom_adj_score_to_bucket_name` instead. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String bucket name. +Deprecated: use `android_oom_adj_score_to_bucket_name` instead. +Returns STRING: Returns the oom_adj bucket. + +| Argument | Type | Description | +|---|---|---| +| value | INT | oom_adj score. | +| android_appid | INT | android_app id of the process. | + +
+ +
+ +### android.power_rails + +#### Views/Tables + +
+ +**android_power_rails_counters**. Android power rails counters data. For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm NOTE: Requires dedicated hardware - table is only populated on Pixels. + +
+ +TABLE +Android power rails counters data. +For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm +NOTE: Requires dedicated hardware - table is only populated on Pixels. + +| Column | Type | Description | +|---|---|---| +| id | INT | `counter.id` | +| ts | INT | Timestamp of the energy measurement. | +| dur | INT | Time until the next energy measurement. | +| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | +| raw_power_rail_name | STRING | Raw power rail name. | +| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | +| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | +| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | +| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | +| track_id | INT | Power rail track id. Alias of `counter_track.id`. | +| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | + +
+ +
+ +### android.process_metadata + +#### Views/Tables + +
+ +**android_process_metadata**. Data about packages running on the process. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid. | +| pid | INT | Process pid. | +| process_name | STRING | Process name. | +| uid | INT | Android app UID. | +| shared_uid | BOOL | Whether the UID is shared by multiple packages. | +| package_name | STRING | Name of the packages running in this process. | +| version_code | INT | Package version code. | +| debuggable | INT | Whether package is debuggable. | + +
+ +
+ +### android.screenshots + +#### Views/Tables + +
+ +**android_screenshots**. Screenshot slices, used in perfetto UI. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| ts | INT | Slice timestamp. | +| dur | INT | Slice duration, should be typically 0 since screeenshot slices are of instant type. | +| name | STRING | Slice name. | + +
+ +
+ +### android.services + +#### Views/Tables + +
+ +**android_service_bindings**. All service bindings from client app to server app. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| client_oom_score | INT | OOM score of client process making the binding. | +| client_process | STRING | Name of client process making the binding. | +| client_thread | STRING | Name of client thread making the binding. | +| client_pid | INT | Pid of client process making the binding. | +| client_tid | INT | Tid of client process making the binding. | +| client_upid | INT | Upid of client process making the binding. | +| client_utid | INT | Utid of client thread making the binding. | +| client_ts | INT | Timestamp the client process made the request. | +| client_dur | INT | Duration of the client binding request. | +| server_oom_score | INT | OOM score of server process getting bound to. | +| server_process | STRING | Name of server process getting bound to | +| server_thread | STRING | Name of server thread getting bound to. | +| server_pid | INT | Pid of server process getting bound to. | +| server_tid | INT | Tid of server process getting bound to. | +| server_upid | INT | Upid of server process getting bound to. | +| server_utid | INT | Utid of server process getting bound to. | +| server_ts | INT | Timestamp the server process got bound to. | +| server_dur | INT | Duration of the server process handling the binding. | +| token | STRING | Unique binder identifier for the Service binding. | +| act | STRING | Intent action name for the service binding. | +| cmp | STRING | Intent component name for the service binding. | +| flg | STRING | Intent flag for the service binding. | +| bind_seq | INT | Monotonically increasing id for the service binding. | + +
+ +
+ +### android.slices + +#### Functions + +
+ +**android_standardize_slice_name** -\> STRING. Some slice names have params in them + +
+ +
+ +
+ +Some slice names have params in them. This functions removes them to make it +possible to aggregate by name. +Some examples are: +- Lock/monitor contention slices. The name includes where the lock +contention is in the code. That part is removed. +- DrawFrames/ooFrame. The name also includes the frame number. +- Apk/oat/dex loading: The name of the apk is removed +Returns STRING: Simplified name. + +| Argument | Type | Description | +|---|---|---| +| name | STRING | The raw slice name. | + +
+ +
+ +### android.startup.startups + +#### Views/Tables + +
+ +**android_startups**. All activity startups in the trace by startup id. Populated by different scripts depending on the platform version/contents. + +
+ +TABLE +All activity startups in the trace by startup id. +Populated by different scripts depending on the platform version/contents. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of startup start. | +| ts_end | INT | Timestamp of startup end. | +| dur | INT | Startup duration. | +| package | STRING | Package name. | +| startup_type | STRING | Startup type. | + +
+ +
+ +
+ +**android_startup_processes**. Maps a startup to the set of processes that handled the activity start. The vast majority of cases should be a single process + +
+ +TABLE +Maps a startup to the set of processes that handled the activity start. + +The vast majority of cases should be a single process. However it is +possible that the process dies during the activity startup and is respawned. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| upid | INT | Upid of process on which activity started. | +| startup_type | STRING | Type of the startup. | + +
+ +
+ +
+ +**android_startup_threads**. Maps a startup to the set of threads on processes that handled the activity start. + +
+ +VIEW +Maps a startup to the set of threads on processes that handled the +activity start. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of start. | +| dur | INT | Duration of startup. | +| upid | INT | Upid of process involved in startup. | +| utid | INT | Utid of the thread. | | thread_name | STRING | Name of the thread. | -| upid | JOINID(process.id) | Id of the process the slice is running on. | -| process_name | STRING | Name of the process. | -| state | STRING | The scheduling state (from the `thread_state` table). Use the `sched_state_to_human_readable_string` function in the `sched` package to get full name. | -| io_wait | BOOL | If the `state` is uninterruptible sleep, `io_wait` indicates if it was an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if we cannot tell if it was an IO sleep or not. Only available on Android when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | -| blocked_function | STRING | If in uninterruptible sleep (D), the kernel function on which was blocked. Only available on userdebug Android builds when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | -| dur | DURATION | The duration of time the threads slice spent for each (state, io_wait, blocked_function) tuple. | +| is_main_thread | BOOL | Thread is a main thread. | -### slices.with_context +
+ +
+ +
+ +**android_thread_slices_for_all_startups**. All the slices for all startups in trace. Generally, this view should not be used + +
+ +VIEW +All the slices for all startups in trace. + +Generally, this view should not be used. Instead, use one of the view functions related +to the startup slices which are created from this table. + +| Column | Type | Description | +|---|---|---| +| startup_ts | INT | Timestamp of startup. | +| startup_ts_end | INT | Timestamp of startup end. | +| startup_id | INT | Startup id. | +| utid | INT | UTID of thread with slice. | +| thread_name | STRING | Name of thread. | +| is_main_thread | BOOL | Whether it is main thread. | +| arg_set_id | INT | Arg set id. | +| slice_id | INT | Slice id. | +| slice_name | STRING | Name of slice. | +| slice_ts | INT | Timestamp of slice start. | +| slice_dur | INT | Slice duration. | + +
+ +
+ +#### Functions + +
+ +**android_sum_dur_for_startup_and_slice** -\> INT. Returns duration of startup for slice name. Sums duration of all slices of startup with provided name. + +
+ +
+ +
+ +Returns duration of startup for slice name. + +Sums duration of all slices of startup with provided name. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +
+ +**android_sum_dur_on_main_thread_for_startup_and_slice** -\> INT. Returns duration of startup for slice name on main thread. Sums duration of all slices of startup with provided name only on main thread. + +
+ +
+ +
+ +Returns duration of startup for slice name on main thread. + +Sums duration of all slices of startup with provided name only on main thread. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +#### Table Functions + +
+ +**android_slices_for_startup_and_slice_name** . Given a startup id and GLOB for a slice name, returns matching slices with data. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| slice_name | STRING | Glob of the slice. | + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the slice. | +| slice_name | STRING | Name of the slice. | +| slice_ts | INT | Timestamp of start of the slice. | +| slice_dur | INT | Duration of the slice. | +| thread_name | STRING | Name of the thread with the slice. | +| arg_set_id | INT | Arg set id. | + +
+ +
+ +
+ +**android_binder_transaction_slices_for_startup** . Returns binder transaction slices for a given startup id with duration over threshold. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| threshold | DOUBLE | Only return slices with duration over threshold. | + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| slice_dur | INT | Slice duration. | +| thread_name | STRING | Name of the thread with slice. | +| process | STRING | Name of the process with slice. | +| arg_set_id | INT | Arg set id. | +| is_main_thread | BOOL | Whether is main thread. | + +
+ +
+ +### android.startup.time_to_display #### Views/Tables -**thread_slice** +
-VIEW -All thread slices with data about thread, thread track and process. +**android_startup_time_to_display**. Startup metric defintions, which focus on the observable time range: TTID - Time To Initial Display \* https://developer.android.com/topic/performance/vitals/launch-time#time-initial \* end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display \* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD \* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Slice | -| ts | TIMESTAMP | Alias for `slice.ts`. | -| dur | DURATION | Alias for `slice.dur`. | -| category | STRING | Alias for `slice.category`. | -| name | STRING | Alias for `slice.name`. | -| track_id | JOINID(track.id) | Alias for `slice.track_id`. | -| track_name | STRING | Alias for `thread_track.name`. | -| thread_name | STRING | Alias for `thread.name`. | -| utid | JOINID(thread.id) | Alias for `thread.utid`. | -| tid | LONG | Alias for `thread.tid`. | -| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | -| process_name | STRING | Alias for `process.name`. | -| upid | JOINID(process.id) | Alias for `process.upid`. | -| pid | LONG | Alias for `process.pid`. | -| depth | LONG | Alias for `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias for `slice.thread_ts`. | -| thread_dur | LONG | Alias for `slice.thread_dur`. | - -**process_slice** - -VIEW -All process slices with data about process track and process. - -| Column | Type | Description | -|---|---|---| -| id | ID(slice.id) | Slice | -| ts | TIMESTAMP | Alias for `slice.ts`. | -| dur | DURATION | Alias for `slice.dur`. | -| category | STRING | Alias for `slice.category`. | -| name | STRING | Alias for `slice.name`. | -| track_id | JOINID(track.id) | Alias for `slice.track_id`. | -| track_name | STRING | Alias for `process_track.name`. | -| process_name | STRING | Alias for `process.name`. | -| upid | JOINID(process.id) | Alias for `process.upid`. | -| pid | LONG | Alias for `process.pid`. | -| depth | LONG | Alias for `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | -| thread_ts | TIMESTAMP | Alias for `slice.thread_ts`. | -| thread_dur | LONG | Alias for `slice.thread_dur`. | - -**thread_or_process_slice** - -VIEW -All the slices in the trace associated to a thread or a process along -with contextual information about them (e.g. thread name, process name, tid etc). - -| Column | Type | Description | -|---|---|---| -| id | JOINID(slice.id) | Slice | -| ts | TIMESTAMP | Alias for `slice.ts`. | -| dur | DURATION | Alias for `slice.dur`. | -| category | STRING | Alias for `slice.category`. | -| name | STRING | Alias for `slice.name`. | -| track_id | JOINID(track.id) | Alias for `slice.track_id`. | -| track_name | STRING | Alias for `track.name`. | -| thread_name | STRING | Alias for `thread.name`. | -| utid | JOINID(thread.id) | Alias for `thread.utid`. | -| tid | LONG | Alias for `thread.tid` | -| process_name | STRING | Alias for `process.name`. | -| upid | JOINID(process.id) | Alias for `process.upid`. | -| pid | LONG | Alias for `process.pid`. | -| depth | LONG | Alias for `slice.depth`. | -| parent_id | JOINID(slice.id) | Alias for `slice.parent_id`. | -| arg_set_id | ARGSETID | Alias for `slice.arg_set_id`. | - -## Package: appleos - -### appleos.instruments.samples - -#### Views/Tables - -**appleos_instruments_samples_summary_tree** +
TABLE -Table summarising the callstacks captured during all -instruments samples in the trace. - -Specifically, this table returns a tree containing all -the callstacks seen during the trace with `self_count` -equal to the number of samples with that frame as the -leaf and `cumulative_count` equal to the number of -samples with the frame anywhere in the tree. +Startup metric defintions, which focus on the observable time range: +TTID - Time To Initial Display +\* https://developer.android.com/topic/performance/vitals/launch-time#time-initial +\* end of first RenderThread.DrawFrame - bindApplication +TTFD - Time To Full Display +\* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD +\* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication +Googlers: see go/android-performance-metrics-glossary for details. | Column | Type | Description | |---|---|---| -| id | LONG | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | -| parent_id | LONG | The id of the parent callstack for this callstack. | -| name | STRING | The function name of the frame for this callstack. | -| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, or JIT. | -| source_file | STRING | The name of the file containing the function. | -| line_number | LONG | The line number in the file the function is located at. | -| self_count | LONG | The number of samples with this function as the leaf frame. | -| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | +| startup_id | INT | Startup id. | +| time_to_initial_display | INT | Time to initial display (TTID) | +| time_to_full_display | INT | Time to full display (TTFD) | +| ttid_frame_id | INT | `android_frames.frame_id` of frame for initial display | +| ttfd_frame_id | INT | `android_frames.frame_id` of frame for full display | +| upid | INT | `process.upid` of the startup | + +
+ +
+ +### android.statsd + +#### Views/Tables + +
+ +**android_statsd_atoms**. Statsd atoms. A subset of the slice table containing statsd atom instant events. + +
+ +VIEW +Statsd atoms. + +A subset of the slice table containing statsd atom instant events. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | INT | The timestamp at the start of the slice (in nanoseconds). | +| dur | INT | The duration of the slice (in nanoseconds). | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_instruction_count | INT | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | INT | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | INT | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | INT | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| thread_ts | INT | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | INT | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | + +
+ +
+ +### android.suspend + +#### Views/Tables + +
+ +**android_suspend_state**. Table of suspended and awake slices. Selects either the minimal or full ftrace source depending on what's available, marks suspended periods, and complements them to give awake periods. + +
+ +TABLE +Table of suspended and awake slices. + +Selects either the minimal or full ftrace source depending on what's +available, marks suspended periods, and complements them to give awake +periods. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| power_state | STRING | 'awake' or 'suspended' | + +
+ +
+ +### android.winscope.inputmethod + +#### Views/Tables + +
+ +**android_inputmethod_clients**. Android inputmethod clients state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_manager_service**. Android inputmethod manager service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_service**. Android inputmethod service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.viewcapture + +#### Views/Tables + +
+ +**android_viewcapture**. Android viewcapture (from android.viewcapture data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.windowmanager + +#### Views/Tables + +
+ +**android_windowmanager**. Android WindowManager (from android.windowmanager data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +## Package: chrome + +### chrome.chrome_scrolls + +#### Views/Tables + +
+ +**chrome_scrolls**. Defines slices for all of the individual scrolls in a trace based on the LatencyInfo-based scroll definition. NOTE: this view of top level scrolls is based on the LatencyInfo definition of a scroll, which differs subtly from the definition based on EventLatencies. TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ WebView instances + +
+ +TABLE +Defines slices for all of the individual scrolls in a trace based on the +LatencyInfo-based scroll definition. + +NOTE: this view of top level scrolls is based on the LatencyInfo definition +of a scroll, which differs subtly from the definition based on +EventLatencies. +TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ +WebView instances. Currently gesture_scroll_id unique within an instance, but +is not unique across multiple instances. Switching to an EventLatency based +definition of scrolls should resolve this. + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the scroll. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| gesture_scroll_begin_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | +| gesture_scroll_end_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | + +
+ +
+ +### chrome.cpu_powerups + +#### Views/Tables + +
+ +**chrome_cpu_power_slice**. The CPU power transitions in the trace. Power states are encoded as non-negative integers, with zero representing full-power operation and positive values representing increasingly deep sleep states. On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep state that the CPU enters while idle. + +
+ +VIEW +The CPU power transitions in the trace. +Power states are encoded as non-negative integers, with zero representing +full-power operation and positive values representing increasingly deep +sleep states. + +On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep +state that the CPU enters while idle. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The CPU on which the transition occurred | +| power_state | INT | The power state that the CPU was in at time 'ts' for duration 'dur'. | +| previous_power_state | INT | The power state that the CPU was previously in. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_first_sched_slice_after_powerup**. The Linux scheduler slices that executed immediately after a CPU power up. + +
+ +TABLE +The Linux scheduler slices that executed immediately after a +CPU power up. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The cpu on which the slice executed. | +| sched_id | INT | Id for the sched_slice table. | +| utid | INT | Unique id for the thread that ran within the slice. | +| previous_power_state | INT | The CPU's power state before this slice. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_post_powerup_slice**. A table holding the slices that executed within the scheduler slice that ran on a CPU immediately after power-up. + +
+ +TABLE +A table holding the slices that executed within the scheduler +slice that ran on a CPU immediately after power-up. + +| Column | Type | Description | +|---|---|---| +| ts | None | Timestamp of the resulting slice | +| dur | None | Duration of the slice. | +| cpu | None | The CPU the sched slice ran on. | +| utid | None | Unique thread id for the slice. | +| sched_id | None | 'id' field from the sched_slice table. | +| type | None | From the sched_slice table, always 'sched_slice'. | +| end_state | None | The ending state for the sched_slice | +| priority | None | The kernel thread priority | +| slice_id | None | Id of the top-level slice for this (sched) slice. | + +
+ +
+ +
+ +**chrome_cpu_power_first_toplevel_slice_after_powerup**. The first top-level slice that ran after a CPU power-up. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | ID of the slice in the slice table. | +| previous_power_state | INT | The power state of the CPU prior to power-up. | + +
+ +
+ +### chrome.event_latency + +#### Views/Tables + +
+ +**chrome_event_latencies**. All EventLatency slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| is_presented | BOOL | Whether this input event was presented. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +
+ +**chrome_gesture_scroll_events**. All scroll-related events (frames) including gesture scroll updates, begins and ends with respective scroll ids and start/end timestamps, regardless of being presented + +
+ +TABLE +All scroll-related events (frames) including gesture scroll updates, begins +and ends with respective scroll ids and start/end timestamps, regardless of +being presented. This includes pinches that were presented. See b/315761896 +for context on pinches. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +#### Functions + +
+ +**chrome_get_most_recent_scroll_begin_id** -\> INT. Extracts scroll id for the EventLatency slice at `ts`. + +
+ +
+ +
+ +Returns INT: The event_latency_id of the EventLatency slice with the type GESTURE_SCROLL_BEGIN that is the closest to `ts`. + +| Argument | Type | Description | +|---|---|---| +| ts | INT | Timestamp of the EventLatency slice to get the scroll id for. | + +
+ +
+ +### chrome.event_latency_description + +#### Views/Tables + +
+ +**chrome_event_latency_stage_descriptions**. Source of truth of the descriptions of EventLatency stages. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | A description of the EventLatency stage. | + +
+ +
+ +### chrome.histograms + +#### Views/Tables + +
+ +**chrome_histograms**. A helper view on top of the histogram events emitted by Chrome. Requires "disabled-by-default-histogram_samples" Chrome category. + +
+ +TABLE +A helper view on top of the histogram events emitted by Chrome. +Requires "disabled-by-default-histogram_samples" Chrome category. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the histogram. | +| value | INT | The value of the histogram sample. | +| ts | INT | Alias of \|slice.ts\|. | +| thread_name | STRING | Thread name. | +| utid | INT | Utid of the thread. | +| tid | INT | Tid of the thread. | +| process_name | STRING | Process name. | +| upid | INT | Upid of the process. | +| pid | INT | Pid of the process. | + +
+ +
+ +### chrome.interactions + +#### Views/Tables + +
+ +**chrome_interactions**. All critical user interaction events, including type and table with associated metrics. + +
+ +TABLE +All critical user interaction events, including type and table with +associated metrics. + +| Column | Type | Description | +|---|---|---| +| scoped_id | INT | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | +| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | +| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | +| ts | INT | Timestamp of the CUI event. | +| dur | INT | Duration of the CUI event. | + +
+ +
+ +### chrome.metadata + +#### Functions + +
+ +**chrome_hardware_class** -\> STRING. Returns hardware class of the device, often use to find device brand and model. + +
+ +
+ +
+ +Returns hardware class of the device, often use to find device brand +and model. +Returns STRING: Hardware class name. + +
+ +
+ +### chrome.page_loads + +#### Views/Tables + +
+ +**chrome_page_loads**. Chrome page loads, including associated high-level metrics and properties. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | +| navigation_id | INT | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | +| navigation_start_ts | INT | Timestamp of the start of navigation. | +| fcp | INT | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | +| fcp_ts | INT | Timestamp of the first contentful paint. | +| lcp | INT | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | +| lcp_ts | INT | Timestamp of the largest contentful paint. | +| dom_content_loaded_event_ts | INT | Timestamp of the DomContentLoaded event: https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event | +| load_event_ts | INT | Timestamp of the window load event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event | +| mark_fully_loaded_ts | INT | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | +| mark_fully_visible_ts | INT | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | +| mark_interactive_ts | INT | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | +| url | STRING | URL at the page load event. | +| browser_upid | INT | The unique process id (upid) of the browser process where the page load occurred. | + +
+ +
+ +### chrome.scroll_interactions + +#### Views/Tables + +
+ +**chrome_scroll_interactions**. Top level scroll events, with metrics. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for an individual scroll. | +| name | STRING | Name of the scroll event. | +| ts | INT | Start timestamp of the scroll. | +| dur | INT | Duration of the scroll. | +| frame_count | INT | The total number of frames in the scroll. | +| vsync_count | INT | The total number of vsyncs in the scroll. | +| missed_vsync_max | INT | The maximum number of vsyncs missed during any and all janks. | +| missed_vsync_sum | INT | The total number of vsyncs missed during any and all janks. | +| delayed_frame_count | INT | The number of delayed frames. | +| predictor_janky_frame_count | INT | The number of frames that are deemed janky to the human eye after Chrome has applied its scroll prediction algorithm. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +### chrome.scroll_jank.predictor_error + +#### Views/Tables + +
+ +**chrome_predictor_error**. The scrolling offsets and predictor jank values for the actual (applied) scroll events. + +
+ +TABLE +The scrolling offsets and predictor jank values for the actual (applied) +scroll events. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| present_ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | +| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | +| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | +| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | +| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | +| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_map + +#### Views/Tables + +
+ +**chrome_scroll_jank_cause_descriptions**. Source of truth of the descriptions of EventLatency-based scroll jank causes. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| event_latency_stage | STRING | The name of the EventLatency stage. | +| cause_process | STRING | The process where the cause of scroll jank occurred. | +| cause_thread | STRING | The thread where the cause of scroll jank occurred. | +| cause_description | STRING | A description of the cause of scroll jank. | + +
+ +
+ +
+ +**chrome_scroll_jank_causes_with_event_latencies**. Combined description of scroll jank cause and associated event latency stage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | Description of the EventLatency stage. | +| cause_process | STRING | The process name that may cause scroll jank. | +| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | +| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_utils + +#### Table Functions + +
+ +**chrome_select_scroll_jank_cause_thread** . Function to retrieve the thread id of the thread on a particular process if there are any slices during a particular EventLatency slice duration; this upid/thread combination refers to a cause of Scroll Jank. + +
+ +
+ +
+ +Function to retrieve the thread id of the thread on a particular process if +there are any slices during a particular EventLatency slice duration; this +upid/thread combination refers to a cause of Scroll Jank. +Argument \| Type \| Description +--- \| --- \| --- +event_latency_id \| INT \| The slice id of an EventLatency slice. +process_type \| STRING \| The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. +thread_name \| STRING \| The name of the thread. + +| Column | Type | Description | +|---|---|---| +| utid | INT | The utid associated with | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_intervals + +#### Views/Tables + +
+ +**chrome_janky_event_latencies_v3**. Selects EventLatency slices that correspond with janks in a scroll + +
+ +TABLE +Selects EventLatency slices that correspond with janks in a scroll. This is +based on the V3 version of scroll jank metrics. + +| Column | Type | Description | +|---|---|---| +| id | INT | The slice id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| track_id | INT | The track_id for the slice. | +| name | STRING | The name of the slice (EventLatency). | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| frame_jank_ts | INT | The start timestamp where frame presentation was delayed. | +| frame_jank_dur | INT | The duration in ms of the delay in frame presentation. | + +
+ +
+ +
+ +**chrome_janky_frame_presentation_intervals**. Frame presentation interval is the delta between when the frame was supposed to be presented and when it was actually presented. + +
+ +VIEW +Frame presentation interval is the delta between when the frame was supposed +to be presented and when it was actually presented. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| event_latency_id | INT | The id of the associated event latency in the slice table. | + +
+ +
+ +
+ +**chrome_scroll_stats**. Scroll jank frame presentation stats for individual scrolls. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | Id of the individual scroll. | +| frame_count | INT | The number of frames in the scroll. | +| missed_vsyncs | INT | The number of missed vsyncs in the scroll. | +| presented_frame_count | INT | The number presented frames in the scroll. | +| janky_frame_count | INT | The number of janky frames in the scroll. | +| janky_frame_percent | FLOAT | The % of frames that janked in the scroll. | + +
+ +
+ +
+ +**chrome_scroll_jank_intervals_v3**. Defines slices for all of janky scrolling intervals in a trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the janky interval. | +| ts | INT | The start timestamp of the janky interval. | +| dur | INT | The duration of the janky interval. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3 + +#### Views/Tables + +
+ +**chrome_gesture_scroll_updates**. Grabs all gesture updates with respective scroll ids and start/end timestamps, regardless of being presented. + +
+ +TABLE +Grabs all gesture updates with respective scroll ids and start/end +timestamps, regardless of being presented. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| id | INT | Slice id for the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_presented_gesture_scrolls**. Scroll updates, corresponding to all input events that were converted to a presented scroll update. + +
+ +TABLE +Scroll updates, corresponding to all input events that were converted to a +presented scroll update. + +| Column | Type | Description | +|---|---|---| +| id | INT | Minimum slice id for input presented in this frame, the non-presented input. | +| ts | INT | The start timestamp for producing the frame. | +| dur | INT | The duration between producing and presenting the frame. | +| last_presented_input_ts | INT | The timestamp of the last input that arrived and got presented in the frame. | +| scroll_update_id | INT | The id of the scroll update event, a unique identifier to the gesture. | +| scroll_id | INT | The id of the ongoing scroll. | +| presentation_timestamp | INT | Frame presentation timestamp. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_scroll_updates_with_deltas**. Associate every trace_id with it's perceived delta_y on the screen after prediction. + +
+ +TABLE +Associate every trace_id with it's perceived delta_y on the screen after +prediction. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | INT | The id of the scroll update event. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | + +
+ +
+ +
+ +**chrome_full_frame_view**. Obtain the subset of input events that were fully presented. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_full_frame_delta_view**. Join deltas with EventLatency data. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_merged_frame_view**. Group all gestures presented at the same timestamp together in a single row. + +
+ +TABLE +Group all gestures presented at the same timestamp together in +a single row. + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| max_start_ts | INT | The timestamp of the last presented input. | +| min_start_ts | INT | The earliest frame start timestamp. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | +| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | +| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Maximum duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_frame_info_with_delay**. View contains all chrome presented frames during gesture updates while calculating delay since last presented which usually should equal to \|VSYNC_INTERVAL\| if no jank is present. + +
+ +TABLE +View contains all chrome presented frames during gesture updates +while calculating delay since last presented which usually should +equal to \|VSYNC_INTERVAL\| if no jank is present. + +| Column | Type | Description | +|---|---|---| +| id | INT | gesture scroll slice id. | +| max_start_ts | INT | OS timestamp of the last touch move arrival within a frame. | +| min_start_ts | INT | OS timestamp of the first touch move arrival within a frame. | +| scroll_id | INT | The scroll which the touch belongs to. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | +| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | +| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives \|total_delta\|. | +| event_latency_id | INT | Event latency id of the presented frame. | +| dur | INT | Duration of the EventLatency. | +| presentation_timestamp | INT | Timestamp at which the frame was shown on the screen. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals \|VSYNC\| if no frame drops happened. | +| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_vsyncs**. Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the minimum delay between frames larger than zero. TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +
+ +TABLE +Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the +minimum delay between frames larger than zero. + +TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +| Column | Type | Description | +|---|---|---| +| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | + +
+ +
+ +
+ +**chrome_janky_frames_no_cause**. Filter the frame view only to frames that had missed vsyncs. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_janky_frames_no_subcause**. Janky frame information including the jank cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | +| cause_id | INT | Id of the slice corresponding to the offending stage. | + +
+ +
+ +
+ +**chrome_janky_frames**. Finds all causes of jank for all janky frames, and a cause of sub jank if the cause of jank was GPU related. + +
+ +TABLE +Finds all causes of jank for all janky frames, and a cause of sub jank +if the cause of jank was GPU related. + +| Column | Type | Description | +|---|---|---| +| cause_of_jank | STRING | The reason the Vsync was missed. | +| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | + +
+ +
+ +
+ +**chrome_unique_frame_presentation_ts**. Counting all unique frame presentation timestamps. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| presentation_timestamp | INT | The unique frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_janky_frames_percentage**. Dividing missed frames over total frames to get janky frame percentage. This represents the v3 scroll jank metrics. Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +
+ +TABLE +Dividing missed frames over total frames to get janky frame percentage. +This represents the v3 scroll jank metrics. +Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +| Column | Type | Description | +|---|---|---| +| delayed_frame_percentage | FLOAT | The percent of missed frames relative to total frames - aka the percent of janky frames. | + +
+ +
+ +
+ +**chrome_frames_per_scroll**. Number of frames and janky frames per scroll. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| num_frames | INT | The number of frames in the scroll. | +| num_janky_frames | INT | The number of delayed/janky frames. | +| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | + +
+ +
+ +
+ +**chrome_causes_per_scroll**. Scroll jank causes per scroll. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| max_delay_since_last_frame | DOUBLE | The maximum time a frame was delayed after the presentation of the previous frame. | +| vsync_interval | DOUBLE | The expected vsync interval. | +| scroll_jank_causes | BYTES | A proto amalgamation of each scroll jank cause including cause name, sub cause and the duration of the delay since the previous frame was presented. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3_cause + +#### Functions + +
+ +**chrome_get_v3_jank_cause_id** -\> LONG. Given two slice Ids A and B, find the maximum difference between the durations of it's direct children with matching names for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) and slice B has children named (X, Y) with durations of (9, 9), the function will return the slice id of the slice named Z that is A's child, as no matching slice named Z was found under B, making 5 - 0 = 5 the maximum delta between both slice's direct children + +
+ +
+ +
+ +Given two slice Ids A and B, find the maximum difference +between the durations of it's direct children with matching names +for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) +and slice B has children named (X, Y) with durations of (9, 9), the function will return +the slice id of the slice named Z that is A's child, as no matching slice named Z was found +under B, making 5 - 0 = 5 the maximum delta between both slice's direct children +Returns LONG: The slice id of the breakdown that has the maximum duration delta. + +| Argument | Type | Description | +|---|---|---| +| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | +| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to \|janky_slice_id\|. | + +
+ +
+ +### chrome.scroll_jank.scroll_offsets + +#### Views/Tables + +
+ +**chrome_scroll_input_offsets**. The raw coordinates and pixel offsets for all input events which were part of a scroll. + +
+ +TABLE +The raw coordinates and pixel offsets for all input events which were part of +a scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Timestamp the of the scroll input event. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +
+ +**chrome_presented_scroll_offsets**. The scrolling offsets for the actual (applied) scroll events + +
+ +TABLE +The scrolling offsets for the actual (applied) scroll events. These are not +necessarily inclusive of all user scroll events, rather those scroll events +that are actually processed. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +### chrome.scroll_jank.utils + +#### Table Functions + +
+ +**chrome_select_long_task_slices** . Extract mojo information for the long-task-tracking scenario for specific names + +
+ +
+ +
+ +Extract mojo information for the long-task-tracking scenario for specific +names. For example, LongTaskTracker slices may have associated IPC +metadata, or InterestingTask slices for input may have associated IPC to +determine whether the task is fling/etc. +Argument \| Type \| Description +--- \| --- \| --- +name \| STRING \| The name of slice. + +| Column | Type | Description | +|---|---|---| +| interface_name | STRING | Name of the interface of the IPC call. | +| ipc_hash | INT | Hash of the IPC call. | +| message_type | STRING | Message type (e.g. reply). | +| id | INT | The slice id. | + +
+ +
+ +### chrome.speedometer + +#### Views/Tables + +
+ +**chrome_speedometer_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_iteration**. Slice that covers one Speedometer iteration. Depending on the Speedometer version these slices might need to be estimated as older versions of Speedometer to not emit marks for this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +Depending on the Speedometer version these slices might need to be estimated +as older versions of Speedometer to not emit marks for this interval. The +metrics associated are the same ones Speedometer would output, but note we +use ns precision (Speedometer uses \~100us) so the actual values might differ +a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_score** -\> DOUBLE. Returns the Speedometer score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer score + +
+ +
+ +
+ +**chrome_speedometer_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_2_1 + +#### Views/Tables + +
+ +**chrome_speedometer_2_1_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async sync is the time between the start and sync-end marks, async is the time between the sync-end and async-end marks. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async +sync is the time between the start and sync-end marks, async is the time +between the sync-end and async-end marks. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_2_1_iteration**. Slice that covers one Speedometer iteration. This slice is actually estimated as a default Speedometer run will not emit marks to cover this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +This slice is actually estimated as a default Speedometer run will not emit +marks to cover this interval. The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. Also note Speedometer +returns the values in ms these here and in ns. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_2_1_score** -\> DOUBLE. Returns the Speedometer 2.1 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 2.1 score + +
+ +
+ +
+ +**chrome_speedometer_2_1_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 2.1 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_3 + +#### Views/Tables + +
+ +**chrome_speedometer_3_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_3_iteration**. Slice that covers one Speedometer iteration. The metrics associated are the same ones Speedometer would output, but note we use ns precision (Speedometer uses \~100us) so the actual values might differ a bit. + +
+ +TABLE +Slice that covers one Speedometer iteration. +The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_3_score** -\> DOUBLE. Returns the Speedometer 3 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 3 score + +
+ +
+ +
+ +**chrome_speedometer_3_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.startups + +#### Views/Tables + +
+ +**chrome_startups**. Chrome startups, including launch cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique ID | +| activity_id | INT | Chrome Activity event id of the launch. | +| name | STRING | Name of the launch start event. | +| startup_begin_ts | INT | Timestamp that the startup occurred. | +| first_visible_content_ts | INT | Timestamp to the first visible content. | +| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | +| browser_upid | INT | Process ID of the Browser where the startup occurred. | + +
+ +
+ +### chrome.tasks + +#### Views/Tables + +
+ +**chrome_java_views**. A list of slices corresponding to operations on interesting (non-generic) Chrome Java views + +
+ +VIEW +A list of slices corresponding to operations on interesting (non-generic) +Chrome Java views. The view is considered interested if it's not a system +(ContentFrameLayout) or generic library (CompositorViewHolder) views. + +TODO(altimin): Add "columns_from slice" annotation. +TODO(altimin): convert this to EXTEND_TABLE when it becomes available. + +| Column | Type | Description | +|---|---|---| +| filtered_name | STRING | Name of the view. | +| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | +| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | +| slice_id | INT | Slice id. | + +
+ +
+ +
+ +**chrome_scheduler_tasks**. A list of tasks executed by Chrome scheduler. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| type | STRING | Type. | +| name | STRING | Name of the task. | +| ts | INT | Timestamp. | +| dur | INT | Duration. | +| utid | INT | Utid of the thread this task run on. | +| thread_name | STRING | Name of the thread this task run on. | +| upid | INT | Upid of the process of this task. | +| process_name | STRING | Name of the process of this task. | +| track_id | INT | Same as slice.track_id. | +| category | STRING | Same as slice.category. | +| depth | INT | Same as slice.depth. | +| parent_id | INT | Same as slice.parent_id. | +| arg_set_id | INT | Same as slice.arg_set_id. | +| thread_ts | INT | Same as slice.thread_ts. | +| thread_dur | INT | Same as slice.thread_dur. | +| posted_from | STRING | Source location where the PostTask was called. | + +
+ +
+ +
+ +**chrome_tasks**. A list of "Chrome tasks": top-level execution units (e.g + +
+ +VIEW +A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / +IPCs / system callbacks) run by Chrome. For a given thread, the slices +corresponding to these tasks will not intersect. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id for the given task, also the id of the slice this task corresponds to. | +| name | STRING | Name for the given task. | +| task_type | STRING | Type of the task (e.g. "scheduler"). | +| thread_name | STRING | Thread name. | +| utid | INT | Utid. | +| process_name | STRING | Process name. | +| upid | INT | Upid. | +| ts | INT | Alias of \|slice.ts\|. | +| dur | INT | Alias of \|slice.dur\|. | +| track_id | INT | Alias of \|slice.track_id\|. | +| category | STRING | Alias of \|slice.category\|. | +| arg_set_id | INT | Alias of \|slice.arg_set_id\|. | +| thread_ts | INT | Alias of \|slice.thread_ts\|. | +| thread_dur | INT | Alias of \|slice.thread_dur\|. | +| full_name | STRING | STRING Legacy alias for \|name\|. | + +
+ +
+ +### chrome.vsync_intervals + +#### Views/Tables + +
+ +**chrome_vsync_intervals**. A simple table that checks the time between VSync (this can be used to determine if we're refreshing at 90 FPS or 60 FPS). Note: In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +
+ +TABLE +A simple table that checks the time between VSync (this can be used to +determine if we're refreshing at 90 FPS or 60 FPS). + +> [!NOTE] +> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Slice id of the vsync slice. | +| ts | INT | Timestamp of the vsync slice. | +| dur | INT | Duration of the vsync slice. | +| track_id | INT | Track id of the vsync slice. | +| time_to_next_vsync | INT | Duration until next vsync arrives. | + +
+ +
+ +#### Functions + +
+ +**chrome_calculate_avg_vsync_interval** -\> FLOAT. Function: compute the average Vysnc interval of the gesture (hopefully this would be either 60 FPS for the whole gesture or 90 FPS but that isnt always the case) on the given time segment. If the trace doesnt contain the VSync TraceEvent we just fall back on assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which corresponds to 16 ms or 60 FPS). + +
+ +
+ +
+ +Function: compute the average Vysnc interval of the +gesture (hopefully this would be either 60 FPS for the whole gesture or 90 +FPS but that isnt always the case) on the given time segment. +If the trace doesnt contain the VSync TraceEvent we just fall back on +assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which +corresponds to 16 ms or 60 FPS). +Returns FLOAT: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. + +| Argument | Type | Description | +|---|---|---| +| begin_ts | LONG | Interval start time. | +| end_ts | LONG | Interval end time. | + +
+ +
+ +### chrome.web_content_interactions + +#### Views/Tables + +
+ +**chrome_web_content_interactions**. Chrome web content interactions (InteractionToFirstPaint), including associated high-level metrics and properties. Multiple events may occur for the same interaction; each row in this table represents the primary (longest) event for the interaction. Web content interactions are discrete, as opposed to sustained (e.g. scrolling); and only occur with the web content itself, as opposed to other parts of Chrome (e.g + +
+ +TABLE +Chrome web content interactions (InteractionToFirstPaint), including +associated high-level metrics and properties. + +Multiple events may occur for the same interaction; each row in this table +represents the primary (longest) event for the interaction. + +Web content interactions are discrete, as opposed to sustained (e.g. +scrolling); and only occur with the web content itself, as opposed to other +parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, +keyboard input (typing), and drags. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for this interaction. | +| ts | INT | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | +| dur | INT | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | +| interaction_type | STRING | The interaction type. | +| total_duration_ms | INT | The total duration of all events that occurred for the same interaction. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +## Package: counters + +### counters.intervals + +#### Macros + +
+ +**counter_leading_intervals** . For a given counter timeline (e.g + +
+ +
+ +
+ +For a given counter timeline (e.g. a single counter track), returns +intervals of time where the counter has the same value. + +Intervals are computed in a "forward-looking" way. That is, if a counter +changes value at some timestamp, it's assumed it *just* reached that +value and it should continue to have that value until the next +value change. The final value is assumed to hold until the very end of +the trace. + +For example, suppose we have the following data: +`ts=0, value=10, track_id=1 +ts=0, value=10, track_id=2 +ts=10, value=10, track_id=1 +ts=10, value=20, track_id=2 +ts=20, value=30, track_id=1 +[end of trace at ts = 40]` + +Then this macro will generate the following intervals: +`ts=0, dur=20, value=10, track_id=1 +ts=20, dur=10, value=30, track_id=1 +ts=0, dur=10, value=10, track_id=2 +ts=10, dur=30, value=20, track_id=2` +Returns: TableOrSubquery, Table with the schema (id UINT32, ts UINT64, dur UINT64, track_id UINT64, value DOUBLE, next_value DOUBLE, delta_value DOUBLE). + +| Argument | Type | Description | +|---|---|---| +| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | + +
+ +
## Package: graphs @@ -6302,17 +6326,25 @@ samples with the frame anywhere in the tree. #### Macros -**graph_dominator_tree** +
+ +**graph_dominator_tree** . Given a table containing a directed flow-graph and an entry node, computes the "dominator tree" for the graph + +
+ +
+ +
Given a table containing a directed flow-graph and an entry node, computes the "dominator tree" for the graph. See \[1\] for an explanation of what a dominator tree is. -\[1\] +\[1\] https://en.wikipedia.org/wiki/Dominator_(graph_theory) Example usage on traces containing heap graphs: - -\`CREATE PERFETTO VIEW dominator_compatible_heap_graph AS +\`\`\` +CREATE PERFETTO VIEW dominator_compatible_heap_graph AS -- Extract the edges from the heap graph which correspond to references -- between objects. SELECT @@ -6335,18 +6367,32 @@ SELECT \* FROM graph_dominator_tree!( dominator_compatible_heap_graph, (SELECT max(id) + 1 FROM heap_graph_object) -);\`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, dominator_node_id LONG). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. +); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, dominator_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. | Argument | Type | Description | |---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with | -| root_node_id | Expr | The entry node to | +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with \|root_node_id\| (see below) being the "entry node" for this component. Specifically, all nodes *must* be reachable by following paths from the root node. Failing to adhere to this property will result in undefined behaviour. If working with a "forest"-like structure, a dummy node should be added which links all the roots of the forest together into a single component; an example of this can be found in the heap graph example query above. | +| root_node_id | Expr | The entry node to \|graph_table\| which will be the root of the dominator tree. | + +
+ +
### graphs.partition #### Macros -**tree_structural_partition_by_group** +
+ +**tree_structural_partition_by_group** . Partitions a tree into a forest of trees based on a given grouping key in a structure-preserving way. Specifically, for each tree in the output forest, all the nodes in that tree have the same ancestors and descendants as in the original tree *iff* that ancestor/descendent belonged to the same group. Example: Input id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 4 \| 1 Or as a graph: `1 (1) / 2 (1) / \ 3 (2) 4 (2) / \ 5 (1) 8 (1) / \ 6 (3) 7 (2)` Possible output (order of rows is implementation-defined) id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 2 \| 1 Or as a forest: `1 (1) 3 (2) 4 (2) 6 (3) | | 2 (1) 7 (2) / \ 5 (1) 8 (1)` + +
+ +
+ +
Partitions a tree into a forest of trees based on a given grouping key in a structure-preserving way. @@ -6370,7 +6416,6 @@ Input | 8 | 4 | 1 | Or as a graph: - `1 (1) / 2 (1) @@ -6379,7 +6424,8 @@ Or as a graph: / \ 5 (1) 8 (1) / \ -6 (3) 7 (2)` Possible output (order of rows is implementation-defined) +6 (3) 7 (2)` +Possible output (order of rows is implementation-defined) | id | parent_id | group_key | |---|---|---| @@ -6393,22 +6439,34 @@ Or as a graph: | 8 | 2 | 1 | Or as a forest: - `1 (1) 3 (2) 4 (2) 6 (3) | | 2 (1) 7 (2) / \ -5 (1) 8 (1)`Returns: TableOrSubquery, The returned table has the schema (id LONG, parent_id LONG, group_key LONG). +5 (1) 8 (1)` +Returns: TableOrSubquery, The returned table has the schema (id UINT32, parent_id UINT32, group_key UINT32). | Argument | Type | Description | |---|---|---| | tree_table | TableOrSubquery | A table/view/subquery corresponding to a tree which should be partitioned. This table must have the columns "id", "parent_id" and "group_key". Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +
+ +
+ ### graphs.search #### Macros -**graph_reachable_dfs** +
+ +**graph_reachable_dfs** . Computes the "reachable" set of nodes in a directed graph from a given set of starting nodes by performing a depth-first search on the graph + +
+ +
+ +
Computes the "reachable" set of nodes in a directed graph from a given set of starting nodes by performing a depth-first search on the graph. The @@ -6420,7 +6478,6 @@ it is primarily intended as a lower-level building block upon which higher level functions/macros in the standard library can be built. Example usage on traces containing heap graphs: - `-- Compute the reachable nodes from the first heap root. SELECT * FROM graph_reachable_dfs!( @@ -6432,14 +6489,27 @@ FROM heap_graph_reference WHERE owned_id IS NOT NULL ), (SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) -);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, parent_node_id LONG). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. | Argument | Type | Description | |---|---|---| | graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | | start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | -**graph_reachable_bfs** +
+ +
+ +
+ +**graph_reachable_bfs** . Computes the "reachable" set of nodes in a directed graph from a given starting node by performing a breadth-first search on the graph + +
+ +
+ +
Computes the "reachable" set of nodes in a directed graph from a given starting node by performing a breadth-first search on the graph. The returned @@ -6451,7 +6521,6 @@ it is primarily intended as a lower-level building block upon which higher level functions/macros in the standard library can be built. Example usage on traces containing heap graphs: - `-- Compute the reachable nodes from all heap roots. SELECT * FROM graph_reachable_bfs!( @@ -6463,21 +6532,33 @@ FROM heap_graph_reference WHERE owned_id IS NOT NULL ), (SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) -);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, parent_node_id LONG). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. | Argument | Type | Description | |---|---|---| | graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | | start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | -**graph_next_sibling** +
+ +
+ +
+ +**graph_next_sibling** . Computes the next sibling node in a directed graph + +
+ +
+ +
Computes the next sibling node in a directed graph. The next node under a parent node is determined by on the \|sort_key\|, which should be unique for every node under a parent. The order of the next sibling is undefined if the \|sort_key\| is not unique. Example usage: - `-- Compute the next sibling: SELECT * FROM graph_next_sibling!( @@ -6488,13 +6569,26 @@ parent_id AS node_parent_id, ts AS sort_key FROM slice ) -);`Returns: TableOrSubquery, The returned table has the schema (node_id LONG, next_node_id LONG). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, next_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. | Argument | Type | Description | |---|---|---| | graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph for which to find the next sibling. This table must have the columns "node_id", "node_parent_id" and "sort_key". | -**graph_reachable_weight_bounded_dfs** +
+ +
+ +
+ +**graph_reachable_weight_bounded_dfs** . Computes the "reachable" set of nodes in a directed graph from a set of starting (root) nodes by performing a depth-first search from each root node on the graph. The search is bounded by the sum of edge weights on the path and the root node specifies the max weight (inclusive) allowed before stopping the search. The returned nodes are structured as a tree with parent-child relationships corresponding to the order in which nodes were encountered by the DFS + +
+ +
+ +
Computes the "reachable" set of nodes in a directed graph from a set of starting (root) nodes by performing a depth-first search from each root node on the graph. @@ -6509,8 +6603,8 @@ it is primarily intended as a lower-level building block upon which higher level functions/macros in the standard library can be built. Example usage on traces with sched info: - -\`-- Compute the reachable nodes from a sched wakeup chain +\`\`\` +-- Compute the reachable nodes from a sched wakeup chain INCLUDE PERFETTO MODULE sched.thread_executing_spans; SELECT \* @@ -6529,238 +6623,19 @@ SELECT id AS root_node_id, id - COALESCE(prev_id, id) AS root_target_weight FROM _wakeup_chain -));\`Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id LONG, parent_node_id LONG). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. +)); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. | Argument | Type | Description | |---|---|---| | graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph and an "edge_weight" corresponding to the weight of the edge between the node. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | -| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to | +| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to \|graph_table\| which will be the roots of the reachability trees. This table must have the columns "root_node_id" and "root_target_weight" corresponding to the starting node id and the max weight allowed on the tree. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | | is_target_weight_floor | Expr | Whether the target_weight is a floor weight or ceiling weight. If it's floor, the search stops right after we exceed the target weight, and we include the node that pushed just passed the target. If ceiling, the search stops right before the target weight and the node that would have pushed us passed the target is not included. | -## Package: sched +
-### sched.latency - -#### Views/Tables - -**sched_latency_for_running_interval** - -TABLE -Scheduling latency of running thread states. -For each time the thread was running, returns the duration of the runnable -state directly before. - -| Column | Type | Description | -|---|---|---| -| thread_state_id | JOINID(thread_state.id) | Running state of the thread. | -| sched_id | JOINID(sched.id) | Id of a corresponding slice in a `sched` table. | -| utid | JOINID(thread.id) | Thread with running state. | -| runnable_latency_id | JOINID(thread_state.id) | Runnable state before thread is "running". Duration of this thread state is `latency_dur`. One of `thread_state.id`. | -| latency_dur | LONG | Scheduling latency of thread state. Duration of thread state with `runnable_latency_id`. | - -### sched.runnable - -#### Views/Tables - -**sched_previous_runnable_on_thread** - -TABLE -Previous runnable slice on the same thread. -For each "Running" thread state finds: - -- previous "Runnable" (or runnable preempted) state. -- previous uninterrupted "Runnable" state with a valid waker thread. - -| Column | Type | Description | -|---|---|---| -| id | JOINID(thread_state.id) | Running thread state | -| prev_runnable_id | JOINID(thread_state.id) | Previous runnable thread state. | -| prev_wakeup_runnable_id | JOINID(thread_state.id) | Previous runnable thread state with valid waker thread. | - -### sched.states - -#### Functions - -**sched_state_to_human_readable_string** - -Translates a single-letter scheduling state to a human-readable string. -Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). - -| Argument | Type | Description | -|---|---|---| -| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | - -**sched_state_io_to_human_readable_string** - -Translates a single-letter scheduling state and IO wait information to -a human-readable string. -Returns STRING: A human readable string with information about the scheduling state and IO wait. - -| Argument | Type | Description | -|---|---|---| -| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | - -### sched.thread_level_parallelism - -#### Views/Tables - -**sched_runnable_thread_count** - -TABLE -The count of runnable threads over time. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp when the runnable thread count changed to the current value. | -| runnable_thread_count | LONG | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | - -**sched_uninterruptible_sleep_thread_count** - -TABLE -The count of threads in uninterruptible sleep over time. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp when the thread count changed to the current value. | -| uninterruptible_sleep_thread_count | LONG | Number of threads in uninterrutible sleep, covering the range from this timestamp to the next row's timestamp. | - -**sched_active_cpu_count** - -TABLE -The count of active CPUs over time. - -| Column | Type | Description | -|---|---|---| -| ts | TIMESTAMP | Timestamp when the number of active CPU changed. | -| active_cpu_count | LONG | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | - -### sched.time_in_state - -#### Views/Tables - -**sched_time_in_state_for_thread** - -TABLE -The time a thread spent in each scheduling state during it's lifetime. - -| Column | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | Utid of the thread. | -| total_runtime | LONG | Total runtime of thread. | -| state | STRING | One of the scheduling states of kernel thread. | -| time_in_state | LONG | Total time spent in the scheduling state. | -| percentage_in_state | LONG | Percentage of time thread spent in scheduling state in \[0-100\] range. | - -**sched_percentage_of_time_in_state** - -TABLE -Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] -ranges). Sum of all states might be smaller than 100, as those values -are rounded down. - -| Column | Type | Description | -|---|---|---| -| utid | JOINID(thread.id) | Utid of the thread. | -| running | LONG | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | -| runnable | LONG | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | -| runnable_preempted | LONG | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | -| sleeping | LONG | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | -| uninterruptible_sleep | LONG | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | -| other | LONG | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | - -#### Table Functions - -**sched_time_in_state_for_thread_in_interval** - -Time the thread spent each state in a given interval. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -Specifically for any non-trivial subset of thread slices, prefer using -`thread_slice_time_in_state` in the `slices.time_in_state` module for this -purpose instead. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | The start of the interval. | -| dur | DURATION | The duration of the interval. | -| utid | JOINID(thread.id) | The utid of the thread. | - -| Column | Type | Description | -|---|---|---| -| state | STRING | The scheduling state (from the `thread_state` table). Use the `sched_state_to_human_readable_string` function in the `sched` package to get full name. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | -| blocked_function | LONG | If the `state` is uninterruptible sleep, `io_wait` indicates if it was an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if we cannot tell if it was an IO sleep or not. Only available on Android when `sched/sched_blocked_reason` ftrace tracepoint is enabled. | -| dur | DURATION | The duration of time the threads slice spent for each (state, io_wait, blocked_function) tuple. | - -**sched_time_in_state_and_cpu_for_thread_in_interval** - -Time the thread spent each state and cpu in a given interval. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| ts | TIMESTAMP | The start of the interval. | -| dur | DURATION | The duration of the interval. | -| utid | JOINID(thread.id) | The utid of the thread. | - -| Column | Type | Description | -|---|---|---| -| state | STRING | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | -| cpu | LONG | Id of the CPU. | -| blocked_function | LONG | Some states can specify the blocked function. Usually NULL. | -| dur | DURATION | Total time spent with this state, cpu and blocked function. | - -**sched_time_in_state_for_cpu_in_interval** - -Time spent by CPU in each scheduling state in a provided interval. - -This function is only designed to run over a small number of intervals -(10-100 at most). It will be *very slow* for large sets of intervals. - -| Argument | Type | Description | -|---|---|---| -| cpu | LONG | CPU id. | -| ts | TIMESTAMP | Interval start. | -| dur | LONG | Interval duration. | - -| Column | Type | Description | -|---|---|---| -| end_state | STRING | End state. From `sched.end_state`. | -| dur | LONG | Duration in state. | - -### sched.with_context - -#### Views/Tables - -**sched_with_thread_process** - -VIEW -View of scheduling slices with extended information. -It holds slices with kernel thread scheduling information. These slices are -collected when the Linux "ftrace" data source is used with the -"sched/switch" and "sched/wakeup\*" events enabled. - -The rows in this table will always have a matching row in the \|thread_state\| -table with \|thread_state.state\| = 'Running' - -| Column | Type | Description | -|---|---|---| -| id | ID(sched.id) | Unique identifier for this scheduling slice (Running period). | -| ts | TIMESTAMP | The timestamp at the start of the Running period. | -| dur | DURATION | The duration of the Running period. | -| utid | JOINID(thread.id) | Unique identifier of the thread that was running. | -| thread_name | STRING | Name of the thread that was running. | -| upid | JOINID(process.id) | Unique identifier of the process that the thread belongs to. | -| process_name | STRING | Name of the process that the thread belongs to. | -| cpu | LONG | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | -| priority | LONG | The kernel priority that the thread ran at. | +
## Package: export @@ -6768,277 +6643,29 @@ table with \|thread_state.state\| = 'Running' #### Functions -**export_to_firefox_profile** +
+ +**export_to_firefox_profile** -\> STRING. Dumps all trace data as a Firefox profile json string See `Profile` in https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js Also https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html You would probably want to download the generated json and then open at https://https://profiler.firefox.com You can easily do this from the UI via the following SQL `SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` The result will have a link for you to download this json as a file. + +
+ +
+ +
Dumps all trace data as a Firefox profile json string See `Profile` in - +https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js Also - +https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html You would probably want to download the generated json and then open at - +https://https://profiler.firefox.com You can easily do this from the UI via the following SQL `SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` The result will have a link for you to download this json as a file. Returns STRING: Json profile -## Package: traced +
-### traced.stats - -#### Views/Tables - -**traced_clone_flush_latency** - -TABLE -Reports the duration of the flush operation for cloned traces (for each -buffer). - -| Column | Type | Description | -|---|---|---| -| buffer_id | LONG | Id of the buffer (matches the config). | -| duration_ns | LONG | Interval from the start of the clone operation to the end of the flush for this buffer. | - -**traced_trigger_clone_flush_latency** - -TABLE -Reports the delay in finalizing the trace from the trigger that causes the -clone operation. - -| Column | Type | Description | -|---|---|---| -| buffer_id | LONG | Id of the buffer. | -| duration_ns | LONG | Interval from the trigger that caused the clone operation to the end of the flush for this buffer. | - -## Package: pkvm - -### pkvm.hypervisor - -#### Views/Tables - -**pkvm_hypervisor_events** - -VIEW -Events when CPU entered hypervisor. - -| Column | Type | Description | -|---|---|---| -| slice_id | JOINID(slice.id) | Id of the corresponding slice in slice table. | -| cpu | JOINID(cpu.cpu) | CPU that entered hypervisor. | -| ts | TIMESTAMP | Timestamp when CPU entered hypervisor. | -| dur | DURATION | How much time CPU spent in hypervisor. | -| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | - -## Package: stacks - -### stacks.cpu_profiling - -#### Views/Tables - -**cpu_profiling_samples** - -TABLE -Table containing all the timestamped samples of CPU profiling which occurred -during the trace. - -Currently, this table is backed by the following data sources: - -- Linux perf -- Simpleperf proto format -- macOS instruments -- Chrome CPU profiling -- Legacy V8 CPU profiling -- Profiling data in Gecko traces - -| Column | Type | Description | -|---|---|---| -| id | LONG | The id of the sample. | -| ts | TIMESTAMP | The timestamp of the sample. | -| utid | JOINID(thread.id) | The utid of the thread of the sample, if available. | -| tid | LONG | The tid of the sample, if available. | -| thread_name | STRING | The thread name of thread of the sample, if available. | -| ucpu | LONG | The ucpu of the sample, if available. | -| cpu | LONG | The cpu of the sample, if available. | -| callsite_id | LONG | The callsite id of the sample. | - -**cpu_profiling_summary_tree** - -TABLE -Table summarising the callstacks captured during any CPU profiling which -occurred during the trace. - -Specifically, this table returns a tree containing all the callstacks seen -during the trace with `self_count` equal to the number of samples with that -frame as the leaf and `cumulative_count` equal to the number of samples with -the frame anywhere in the tree. - -The data sources supported are the same as the `cpu_profiling_samples` table. - -| Column | Type | Description | -|---|---|---| -| id | LONG | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | -| parent_id | LONG | The id of the parent callstack for this callstack. NULL if this is root. | -| name | STRING | The function name of the frame for this callstack. | -| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | -| source_file | STRING | The name of the file containing the function. | -| line_number | LONG | The line number in the file the function is located at. | -| self_count | LONG | The number of samples with this function as the leaf frame. | -| cumulative_count | LONG | The number of samples with this function appearing anywhere on the callstack. | - -## Package: time - -### time.conversion - -#### Functions - -**time_from_ns** - -Returns the provided nanosecond duration, which is the default -representation of time durations in trace processor. Provided for -consistency with other functions. -Returns TIMESTAMP: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | LONG | Time duration in nanoseconds. | - -**time_from_us** - -Converts a duration in microseconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns LONG: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| micros | LONG | Time duration in microseconds. | - -**time_from_ms** - -Converts a duration in millseconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns TIMESTAMP: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| millis | LONG | Time duration in milliseconds. | - -**time_from_s** - -Converts a duration in seconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns TIMESTAMP: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| seconds | LONG | Time duration in seconds. | - -**time_from_min** - -Converts a duration in minutes to nanoseconds, which is the default -representation of time durations in trace processor. -Returns TIMESTAMP: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| minutes | LONG | Time duration in minutes. | - -**time_from_hours** - -Converts a duration in hours to nanoseconds, which is the default -representation of time durations in trace processor. -Returns TIMESTAMP: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| hours | LONG | Time duration in hours. | - -**time_from_days** - -Converts a duration in days to nanoseconds, which is the default -representation of time durations in trace processor. -Returns LONG: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| days | LONG | Time duration in days. | - -**time_to_ns** - -Returns the provided nanosecond duration, which is the default -representation of time durations in trace processor. Provided for -consistency with other functions. -Returns LONG: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_us** - -Converts a duration in nanoseconds to microseconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in microseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_ms** - -Converts a duration in nanoseconds to millseconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in milliseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_s** - -Converts a duration in nanoseconds to seconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in seconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_min** - -Converts a duration in nanoseconds to minutes. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in minutes. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_hours** - -Converts a duration in nanoseconds to hours. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in hours. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -**time_to_days** - -Converts a duration in nanoseconds to days. Nanoseconds is the default -representation of time durations in trace processor. -Returns LONG: Time duration in days. - -| Argument | Type | Description | -|---|---|---| -| nanos | TIMESTAMP | Time duration in nanoseconds. | - -Except as otherwise noted, the content of this page is licensed under the -[Creative Commons -Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), and code samples are licensed -under the [Apache 2.0 -License](https://www.apache.org/licenses/LICENSE-2.0). Java is a registered trademark of Oracle and/or its affiliates. - -- [Site CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) -- [Privacy](https://www.google.com/intl/en/policies/privacy/) \ No newline at end of file +
\ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/SKILL.md b/profilers/perfetto-trace-analysis/SKILL.md new file mode 100644 index 0000000..6575e25 --- /dev/null +++ b/profilers/perfetto-trace-analysis/SKILL.md @@ -0,0 +1,81 @@ +--- +name: perfetto-trace-analysis +description: Analyzes Perfetto traces to find the root cause of latency, memory, +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-13' + keywords: + - Perfetto + - trace analysis + - Android performance + - jank + - profiling + - debugging + - Perfetto SQL + - performance analysis + - troubleshooting +--- + +or jank issues in Android apps. Use when the user provides a Perfetto trace file +and asks any question, ongoing investigation, or open-ended request to analyze +its contents. + +## Resources + +- **Domain Hints:** Reference files for specific performance areas are located in [`references/hints_[domain].md`](https://developer.android.com/agents/skills/perfetto-trace-analysis/references/hints_*) (e.g., `hints_cpu.md`, `hints_io.md`). To avoid context bloat, do not load all hints at once; read the specific file only when you have narrowed your investigation to that domain. +- **Perfetto SQL Skill:** This workspace may contain a `perfetto-sql` skill that guides translating intents into valid queries. You must search for and locate it, then follow its [Execution Protocol](references/android/agents/skills/profilers/perfetto-sql/skill.md) for all SQL generation. If it is not present, proceed as without it, downloading the trace processor and creating + running the queries to the best of your abilities. + +## Setup Phase (Mandatory) + +1. **Initialize Scratchpad (Chain of Evidence):** + - Maintain your working memory in a local scratchpad file located in the exact same directory as the target trace file. + - Name the file using the trace's filename appended with `_analysis.md` (e.g., `[trace_filename]_analysis.md`). Before creating it, check if a file with that name already exists by listing the directory's contents---to avoid biasing your investigation, DO NOT read the file's contents to check for its existence. If it does, append an incrementing version number (e.g., `_v2.md`, `_v3.md`) until you find an available filename. You MUST hardcode this exact filename in all subsequent tool calls. + - Use this scratchpad STRICTLY to log verified facts: timestamps, slice names, thread IDs (utid/tid), and thread states. + - DO NOT write preliminary hypotheses or premature conclusions in the scratchpad. It is a strict Chain of Evidence. +2. **Locate SQL Skill:** Search your workspace for the `perfetto-sql` skill (e.g., using `glob` or `list_directory`). If available, you MUST locate and follow its [Execution Protocol](references/android/agents/skills/profilers/perfetto-sql/skill.md) for all SQL generation. If it is not present, proceed as normal. Do not guess schemas. +3. **Target Resolution:** If the user's request is broad (e.g., "why is the app slow?") and doesn't specify a package name: + - Execute a query to identify the active application: `sql INCLUDE + PERFETTO MODULE android.startup.startups; SELECT package FROM + android_startups;` + - If multiple packages are returned, ask the user to choose one. Save the chosen `package_name` to your scratchpad. + +## Investigation Protocol + +Follow this iterative loop until you have isolated the definitive root cause(s): + +### 1. Formulate Hypothesis + +- **Prioritization:** Form hypotheses using information from: user prompt \> built-in hints ([`references/hints_*.md`](https://developer.android.com/agents/skills/perfetto-trace-analysis/references/hints_*)) \> general knowledge. +- **Source Attribution:** Explicitly mention the source of your hypothesis (e.g., "Based on hints_io.md..."). +- **Focus Constraint:** Focus on the primary bottleneck. Avoid investigating deep into binder transactions unless the user explicitly asks for it or there is no other obvious bottleneck. +- **State Reasoning:** Briefly state your reasoning based on previous findings *before* generating a new query. + +### 2. Plan and Collect Data + +- **Metrics First:** Start with a high-level view using trace metrics before diving into custom SQL (e.g., `./trace_processor --run-metrics + android_startup`). +- **Broad to Narrow:** Begin with broad queries using minimal filters. Favor fuzzy matching (e.g., `GLOB '*abc*'`) over exact matching. +- **Overlapping Time:** When filtering by time, you MUST check for events that overlap with the target time range (e.g., `start1 < end2 AND start2 < end1`) to ensure you don't miss slices that span across the boundaries. + +### 3. Analyze and Drill Down (Depth-First) + +- **Evidentiary Rigor:** Do not draw conclusions without explicit data. +- **Wall Time vs. CPU Time:** Do not assume a long-running slice is actively computing. You MUST query the `thread_state` table for the exact timestamp window of suspicious slices to verify if the thread was `Running`, `Runnable` (waiting for CPU), or `Sleeping`/`Uninterruptible Sleep` (blocked). +- **Follow Dependencies:** If a thread is blocked/waiting, you MUST find what it is waiting *for* (Binder, Lock, I/O, etc.). Cross process boundaries if necessary. You cannot conclude an investigation on a waiting thread without identifying the blocker. + +### 4. Exhaustive Investigation (Do Not Give Up Early) + +- **Multiple Bottlenecks:** Complex performance issues rarely have a single cause. Do NOT stop your investigation after finding the first anomaly. Even if you find a major bottleneck (e.g., emulator graphics lag), you MUST continue searching for other independent system-wide issues (e.g., lock contention, I/O stalls). +- **Global Verification:** Periodically perform a system-wide query for the longest running slices (`ORDER BY slice.dur DESC`) and most frequent D-states to ensure your local investigation hasn't missed a massive, unrelated system stall. +- **Persist Through Dead Ends:** If a hypothesis is disproven or a query returns empty, do not conclude. Pivot your focus, broaden your search constraints (fuzzy matching, wider time windows), and continue the mission. + +## Final Report + +Only when you have followed the entire chain of dependencies to the root +cause(s) AND confirmed through exhaustive search that no other major bottlenecks +exist: 1. Summarize your findings detailing the verified chain of evidence. 2. +Conclude with: "This concludes the trace analysis. You can review the full chain +of evidence in \[scratchpad_filename\]. Let me know if you would like me to drill +down into any of these specific threads, or if you'd like help drafting a bug +report." diff --git a/profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md b/profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md new file mode 100644 index 0000000..7ab3311 --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md @@ -0,0 +1,126 @@ +## Guidelines and Hints + +- **Idempotency:** Ensure queries are idempotent to prevent "already exists" errors during multiple executions. + - For Perfetto objects, always use `CREATE OR REPLACE`: `CREATE OR REPLACE + PERFETTO TABLE`, `CREATE OR REPLACE PERFETTO VIEW`, `CREATE OR REPLACE + PERFETTO FUNCTION`, `CREATE OR REPLACE PERFETTO MACRO`. + - For SQLite Virtual Tables (such as `SPAN_JOIN`), `CREATE OR REPLACE` is not supported. Explicitly drop them first: `DROP TABLE IF EXISTS + my_table; CREATE VIRTUAL TABLE my_table USING SPAN_JOIN(...);` + - For standard SQLite indexes, prepend `DROP INDEX IF EXISTS index_name;`. +- `SPAN_JOIN` will crash if intervals **within the same input table** overlap. Always use the `PARTITIONED {column}` (for example, `PARTITIONED upid`) clause to isolate intervals. +- Intermediate tables fed into a `SPAN_JOIN` must be materialized using `CREATE PERFETTO TABLE`, not `CREATE VIEW`. +- **Trace Boundaries (`dur = -1`):** Slices or thread states that don't finish before the trace ends are recorded with `dur = -1`. When calculating a bounding box (for example, `ts + dur`) or summing durations (`SUM(dur)`), handle incomplete durations using: `IIF(dur = -1, trace_end() - ts, dur)`. +- **Robust State Transitions:** Avoid manual timestamp arithmetic (for example, `ts + dur = next.ts`) to join adjacent events. Rely on standard library modules (for example, `sched.runnable`, `linux.perf.counters`, `intervals.overlap`) which safely handle trace gaps and preemptions. +- **Unique Identifiers:** When writing SQL queries in Perfetto, you must join tables using `utid` (unique thread ID) or `upid` (unique process ID) instead of the regular `tid` or `pid`. **Why it's useful** : The operating system recycles `TIDs` and `PIDs`, while `UTIDs` and `UPIDs` remain unique for the lifetime of the trace, which prevents incorrect joins. +- **Safe Argument Extraction:** Use `EXTRACT_ARG(arg_set_id, 'key')` to extract dictionary or JSON-like properties from slices or tracks. Don't attempt string parsing. +- **String Matching (Always use GLOB):** Use `GLOB` instead of `LIKE`. `LIKE` causes performance bottlenecks and treats underscores (`_`) as wildcards, leading to bugs. + - **Exact matches:** Use `=`. + - **Substring matches:** Use `GLOB` with `*` (for example, `name GLOB + '*RenderThread*'`). + - **Case-insensitive matches:** Use `LOWER(name) GLOB` and make sure the search string is fully lowercase (for example, `LOWER(name) GLOB + '*renderthread*'`). Use this when dealing with inconsistent trace capitalization (for example, `WakeLock` versus `wakelock`). +- **Calculating Time Overlaps:** To calculate the overlap duration between two + time intervals `[start1, end1]` and `[start2, end2]`: + + > **Precedence Rule:** Always prefer using `SPAN_JOIN` or standard library + > functions (for example, `intervals.overlap`) to calculate overlaps + > **between two different sets of intervals** . Avoid manual arithmetic if a + > standard library feature or `SPAN_JOIN` can achieve the same result. + > Use the following logic if no built-in alternative exists. + 1. **Condition:** The intervals overlap if `start1 < end2` and `start2 < + end1`. + 2. **Duration:** The overlap duration is calculated as `MIN(end1, end2) - + MAX(start1, start2)` + + > **Important:** Incomplete Perfetto slices have a duration + > of -1 (`dur = -1`). Always calculate the effective end time using + > `ts + IIF(dur = -1, trace_end() - ts, dur)` before applying + > this logic. +- Query `android_thread_slices_for_all_startups` for app startup requests. + +- Join `counter_track` with `counter` to get values of counter with a specific + name. + +- When querying for a CPU frequency counter, include the `linux.cpu.frequency` + module and use the `cpu_frequency_counters` table. + +- When looking for events around a specific timestamp, start with 100ms as the + window size. + +- Always prefix column names with table or view alias, that is: + `{alias}.{column_name}`. + +- To calculate the total time spent in slices matching a specific name pattern + (for example, `*{name_pattern}*`), you must sum their durations. **Why it's + useful** : This helps quantify the total impact of a specific function or + feature on performance across multiple calls. Here is an example query + (note the safe handling of incomplete slices): + `sql + SELECT + count(*) as total_count, + sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / 1000000.0 as total_dur_ms + FROM slice + WHERE slice.name GLOB '*{name_pattern}*';` + +## Resources + +- **Documentation:** The Perfetto Standard Library documentation is in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/perfetto-sql/references/perfetto-stdlib-docs). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. + +## Execution Protocol + +You must follow these steps sequentially, mirroring a multi-agent pipeline: + +### Step 0: Tool Setup + +**Fetch the Wrapper:** If you don't have `trace_processor` in your current +working directory or in your path, download it directly from the Perfetto index: +`curl -LO https://get.perfetto.dev/trace_processor && chmod +x +./trace_processor` +> **Important:** The file served at this URL is a `~10KB` Python +> wrapper script. Don't assume the download failed because it is human-readable +> text. This is the intended behavior. This script handles lazy-loading the real +> binary into `~/.local/share/perfetto/` on its first run. Use it directly. + +### Step 1: Dissection and Schema Research + +1. Identify the core question, required data points, and filtering conditions. +2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/perfetto-sql/references/perfetto-stdlib-docs). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: + - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). + - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. + - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. + - **Targeted Bounded Reads:** Once you identify the relevant modules, efficiently read the tables and views within that module section. + - **Extract:** Extract only the schema, columns, and the exact `INCLUDE PERFETTO MODULE` statements for the required object from the documentation. + - **Verify:** Review the columns, types, and descriptions to ensure the table matches your needs. +4. Print the research results before drafting the query: +5. *Tables/Views:* `Schema for {name}:` listing columns and types. + +### Step 2: Draft and Validate Loop (Max 3 Iterations) + +Draft the SQL query in SQLite syntax using **only** the schemas +retrieved in Step 1. After drafting, you must validate against this checklist: + +- \[ \] **SQLite Syntax:** Does the query parse successfully without syntax errors? +- \[ \] **Idempotency:** Are all object creations safe to re-run? (Did you use `CREATE OR REPLACE PERFETTO` and `DROP TABLE IF EXISTS` for virtual tables?) +- \[ \] **Existence:** Were all tables found in the documentation? +- \[ \] **Intent Check:** Is there a pre-existing standard library table or view that will fulfill this intent before instead of writing manual arithmetic? +- \[ \] **Column Accuracy:** Do columns match the retrieved schemas? +- \[ \] **Alias Check:** Are ALL column names prefixed with their table or view alias (for example, `alias.column_name`)? +- \[ \] **Module Check:** Are `INCLUDE PERFETTO MODULE` statements included for all non-prelude modules? **You must use the exact module names provided in + the documentation.** +- \[ \] **Span Join Check:** If using `SPAN_JOIN`, are tables safely `PARTITIONED` to prevent overlapping interval crashes? Are intermediate tables materialized with `CREATE PERFETTO TABLE`? +- \[ \] **No LIKE Constraint:** Did you map string matches using `GLOB` or `=` instead of prohibited `LIKE`? +- \[ \] **Execution Check:** You MUST run queries using the standalone + `./trace_processor` wrapper with the `--query-string` flag: + `./trace_processor --query-string "QUERY" {trace_file}`. + + **Execution Rules:** + - **File Usage** : If you must create a SQL file to execute queries (for example, due to query length or escaping issues), you must create them in the `/tmp/` directory. + - **State:** The execution is purely ephemeral. Database state does not persist across turns. You **cannot** share state (like views or tables) across queries in different turns. Every query must be standalone and fully self-contained. + - **Failure Resilience:** Debug and fix SQL syntax and logic errors when query fails.Don't simplify the analytical intent to pass validation. For example, if requested to calculate an overlap or intersection, you must fix the intersection math. Don't substitute with disjoint queries (for example, returning independent total durations) as a workaround. + +### Step 3: Final Output + +1. Explicitly return and state the final validated SQL and explain the results to the user. +2. Before finishing your response, delete all temporary SQL files you created in `/tmp/` directory. \ No newline at end of file diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md similarity index 75% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md rename to xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index ee09ddf..120a0d0 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -1,19 +1,19 @@ --- -name: display-ai-glasses-with-jetpack-compose-glimmer -description: Provides guidelines for developing projected applications for Android - Display AI Glasses using the Jetpack Compose Glimmer UI toolkit. This skill covers - foundational Glimmer design principles, workflows for implementing Jetpack Compose - Glimmer, and interaction models for the glasses form factor. Use this skill to build - an Android XR Augmented Experience application with Jetpack Compose Glimmer that - adheres to the Glimmer design system for optimized Glasses styling. +name: display-glasses-with-jetpack-compose-glimmer +description: Provides guidelines for developing projected Android XR apps for display + glasses using the Jetpack Compose Glimmer UI toolkit. This skill covers foundational + Glimmer design principles, workflows for implementing Jetpack Compose Glimmer, and + interaction models for the glasses form factor. Use this skill to build an Android + XR Augmented Experience app with Jetpack Compose Glimmer that adheres to the Glimmer + design system for optimized glasses styling. license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-08' + last-updated: '2026-05-13' keywords: - Jetpack Compose Glimmer - - AI Glasses - - Display AI Glasses + - audio glasses + - display glasses - Projected Activity - GlimmerTheme - Additive Display @@ -24,24 +24,27 @@ metadata: | Term | Definition | |---|---| -| **AI Glasses** | All-day wear, hands-free devices that provide access to information. Some have a display and some are audio-only. Equipped with speakers, a camera, and a microphone. | -| **Display AI Glasses** | AI Glasses with the addition of a small, private display for glanceable visuals that harmonize with audio output. | -| **Jetpack Compose Glimmer** | A Compose UI toolkit for building augmented Android XR experiences, optimized for Display AI Glasses. It provides components, theming, and behaviors for transparent displays. | -| **Projected Activity** | An Android `Activity` that runs on a host device (phone) but its UI and interactions are projected to a connected AI glasses device. | +| **Intelligent Eyewear** | All-day wear, hands-free devices that provide access to information. Equipped with speakers, a camera, and a microphone. Some are audio-only (audio glasses), and some also have a display (display glasses). | +| **Display Glasses** | Audio glasses with the addition of a small, private display for glanceable visuals that harmonize with audio output. | +| **Jetpack Compose Glimmer** | A Compose UI toolkit for building augmented Android XR experiences, optimized for display glasses. It provides components, theming, and behaviors for transparent displays. | +| **Projected Activity (Glasses Activity)** | An Android `Activity` that runs on a host device (phone) but its UI and interactions are projected to a connected, intelligent eyewear device (audio or display glasses). | | **Projected Device** | An XR device connected to an Android-powered device (host). Host projects the application content to the Projected device and let users interact with it. | | **GlimmerTheme** | The root provider for styling tokens, including GlimmerColors, GlimmerTypography, and GlimmerShapes. | -| **Additive Display** | A display technology where black (#000000) is rendered as 100% transparent. UI is built by adding light to the environment. Display AI Glasses have an additive display. | +| **Additive Display** | A display technology where black (#000000) is rendered as 100% transparent. UI is built by adding light to the environment. Display glasses have an additive display. | | **Augmented Experiences** | Android XR experiences that enhance a user's focus and presence in the real world. They are lightweight and additive, helping users while they are on-the-go | | **Visual Angle** | A unit of measurement for perceived size in XR. The minimum readable text size is 0.6 degrees (approx. 18sp at 1 m). | ## Prerequisites -- Mobile project should target minSDK 37 or higher. If the target SDK is lower than 37, increase the SDK to 37. AI Glasses should target API 36 or higher. +- Mobile project must target `compileSdk` 37 or higher. If the `compileSdk` is lower than 37, increase the `compileSdk` to 37. +- Ensure you are using the latest library dependencies from [Create your first + activity for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). ## Core Constraints - **Don't:** Use `MaterialTheme` or Material Components. - **Do:** Use `GlimmerTheme` and Jetpack Compose Glimmer Components. +- **Do:** Use `createGoogleSansFlexTypography()` from `androidx.xr.glimmer:glimmer-google-fonts` as the `Typography` value for `GlimmerTheme` to ensure that consistent typography is applied throughout the components. ## Limitations @@ -52,23 +55,24 @@ metadata: ## 1. Set up dependencies -- **Setup Projected Activity:** First you need to create a new projected activity for your AI glasses app. If the project doesn't already have one, see [Create your first activity for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. +- **Setup Projected Activity:** First, you need to create a new projected activity for your app. If the project doesn't already have one, see [Create + your first activity for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. Ensure that you specify `xr_projected` for the `android:requiredDisplayCategory` attribute in app manifest to tell the system that this activity will use a projected context to access hardware from a connected device. - **Mobile App Integration:** If the project contains an existing mobile app, you must create a new Glasses Activity dedicated to rendering Glimmer UI. For detailed configuration, heavily reference [Create your first activity - for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the AI Glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. + for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the display glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. - **UI Library:** Identify if the project has the `androidx.xr.glimmer:glimmer` library, if not it must be added to the project. See [Declaring Jetpack Compose Glimmer Dependencies](https://developer.android.com/jetpack/androidx/releases/xr-glimmer#declaring_dependencies) to fetch the latest dependency version. - **Theming:** All Glimmer components must be wrapped within the `GlimmerTheme` composable to ensure correct token resolution. -- **Mandatory black background:** AI Glasses use additive displays. Any non-black color in the background blocks the real world. **You must always** set a pure black background (`Modifier.background(Color.Black)`) on the root container of your Projected Activity. +- **Mandatory black background:** Display glasses use additive displays. Any non-black color in the background blocks the real world. **You must always** set a pure black background (`Modifier.background(Color.Black)`) on the root container of your Projected Activity. - **Font:** The default font is Google Sans Flex. Use `androidx.xr.glimmer.googlefonts` library with the default type styles unless otherwise specified. Use `createGoogleSansFlexTypography` to create a `Typography` instance with the Google Sans Flex configuration. Provide this `Typography` instance as normal through `GlimmerTheme`. Use [references/glimmersansflextypography-source.md](references/glimmersansflextypography-source.md) for configuration. -- **Hardware Capabilities:** Different types of AI glasses have different capabilities. To check for these at runtime, see the [Check device - capabilities at runtime for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md). -- **Hardware Permissions:** To request hardware permissions like the microphone and camera, see the [Request hardware permissions for AI - glasses](references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md). -- **Hardware Access:** To use the glasses camera, sensors, or access the phone's hardware, see the [Use a projected context to access AI glasses - hardware](references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md). +- **Hardware Capabilities:** Different types of intelligent eyewear devices have different capabilities. To check for these at runtime, see the [Check + device capabilities at runtime for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md). +- **Hardware Permissions:** To request hardware permissions like the microphone and camera, see the [Request hardware permissions for + intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md). +- **Hardware Access:** To use the glasses camera, sensors, or access the phone's hardware, see the [Use a projected context to access hardware on + intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md). ## 2. Minimize and translate the UI -- For Display AI Glasses, build UI using components from the Jetpack Compose Glimmer framework. +- For display glasses, build UI using components from the Jetpack Compose Glimmer framework. - Use depth to communicate element priority and hierarchy. - Design from the bottom up, trying to minimize how much of the real world you cover. Always bottom align UI to the glasses display. - **One Thing at a Time:** Prioritize the user's awareness of the real world. Show only one primary piece of information at a time (for example, using a `Stack`) to minimize obstruction of the user's field of view. Avoid multiple simultaneous cards. @@ -79,17 +83,17 @@ metadata: - Map app interactions, such as tap and swipe, to the available hardware controls on the glasses, such as the touchpad. - Inputs are more 1-dimensional; users typically make one control input at a time. - Avoid nesting scrolling controls. -- Jetpack Compose Glimmer components are designed to work with standard input methods, such as a tap or swipe on the AI glasses' touchpad. +- Jetpack Compose Glimmer components are designed to work with standard input methods, such as a tap or swipe on the glasses' touchpad. - Use System Back to dismiss temporary states or detailed views. - To add input, focus, tap, swipe to your Glasses UI, follow [Focus in Jetpack Compose Glimmer](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md). - For a detailed breakdown of hardware inputs, see [Hardware Controls for - Display AI Glasses](references/android/design/ui/ai-glasses/guides/interaction/inputs.md) + display glasses](references/android/design/ui/ai-glasses/guides/interaction/inputs.md) ## 4. Build with Jetpack Compose Glimmer Jetpack Compose Glimmer is the UI toolkit for building augmented experiences on -Display AI Glasses. +display glasses. ### Key Features @@ -142,9 +146,9 @@ configure the following axes: #### Depth Levels -Simulate depth on AI glasses using shadows to establish a sense of hierarchy -through varying levels of emphasis. The Jetpack Compose Glimmer controls use -`DepthEffect` with 5 preset `DepthEffectLevels`. Use +Simulate depth on display glasses using shadows to establish a sense of +hierarchy through varying levels of emphasis. The Jetpack Compose Glimmer +controls use `DepthEffect` with 5 preset `DepthEffectLevels`. Use [references/deptheffect-source.md](references/deptheffect-source.md) and [references/deptheffectlevels-source.md](references/deptheffectlevels-source.md) for reference. @@ -184,8 +188,9 @@ If you are creating a Glimmer Card component, read the: #### Buttons Buttons are the primary triggers for discrete actions in Glimmer. They are -specifically optimized for the AI Glasses focus model, where a focus highlight -is added when focus is moved to the button using the touchpad or other methods. +specifically optimized for the display glasses focus model, where a focus +highlight is added when focus is moved to the button using the touchpad or other +methods. ##### Core Implementation Logic @@ -307,6 +312,17 @@ text component, and lets you set various text properties. Be sure to choose a style from the `GlimmerTheme` for your text. Modify the theme for your application if you want custom typography. +##### Essential Constraint: Glimmer Text versus Material Text + +On transparent Display Glasses (additive displays), standard Material `Text` +resolves to dark foreground tokens which render as transparent and invisible. +Glimmer `Text` intelligently manages theme color matching. When no manual color +override is specified, Glimmer `Text` automatically defaults to the content +color provided by the nearest Glimmer surface. + +- **Don't:** Use Material Text +- **Do:** Use Glimmer Text + #### Surface `Surface` is a fundamental building block in Glimmer. Use @@ -318,6 +334,4 @@ component. ## 5. Integrate with system UI -- For a detailed breakdown of notifications on AI Glasses, see [Understand - notification behavior for AI glasses](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md) and learn how to [Start a glasses - activity on display AI glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). +- For a detailed breakdown of notifications on intelligent eyewear, see [Understand notification behavior for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md) and learn how to [Start a glasses activity on display glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md similarity index 98% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md index 12fec60..3493977 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md @@ -29,20 +29,20 @@ Add the following [library dependencies for AI glasses](https://developer.androi dependencies { implementation "androidx.xr.runtime:runtime:1.0.0-alpha13" - implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha11" - implementation "androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11" - implementation "androidx.xr.projected:projected:1.0.0-alpha06" - implementation "androidx.xr.arcore:arcore:1.0.0-alpha12" + implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha12" + implementation "androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha12" + implementation "androidx.xr.projected:projected:1.0.0-alpha07" + implementation "androidx.xr.arcore:arcore:1.0.0-alpha13" } ### Kotlin dependencies { implementation("androidx.xr.runtime:runtime:1.0.0-alpha13") - implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha11") - implementation("androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha11") - implementation("androidx.xr.projected:projected:1.0.0-alpha06") - implementation("androidx.xr.arcore:arcore:1.0.0-alpha12") + implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha12") + implementation("androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha12") + implementation("androidx.xr.projected:projected:1.0.0-alpha07") + implementation("androidx.xr.arcore:arcore:1.0.0-alpha13") } ## Declare your activity in your app's manifest diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md similarity index 99% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md index 304e3b2..5d8c4ab 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/button-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-source.md @@ -122,6 +122,7 @@ public fun Button( modifier .semantics { role = Role.Button } .surface( + enabled = enabled, shape = shape, color = color, contentColor = contentColor, diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/card-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/icon-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md similarity index 99% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md index f7e5d6d..59c59c7 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md @@ -91,6 +91,7 @@ public fun IconButton( modifier .semantics { role = Role.Button } .surface( + enabled = enabled, shape = shape, color = color, contentColor = contentColor, diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/list-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/listitem-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/liststate-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/stack-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stack-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/stack-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md similarity index 94% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md index 5f0f057..d5b50ed 100644 --- a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/surface-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/surface-source.md @@ -68,7 +68,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.intellij.lang.annotations.Language -// TODO: b/423573184 align on disabled behavior / state /** * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area * or 'physical' boundary for components such as buttons and cards. A [surface] implements shared @@ -103,6 +102,10 @@ import org.intellij.lang.annotations.Language * Similarly, to create a focusable surface: * * @sample androidx.xr.glimmer.samples.FocusableSurfaceSample + * @param enabled controls the enabled state of this surface. When `false`, a disabled overlay + * visual will be drawn on top of the surface. Note that this only affects the visual decoration; + * it does not intercept input or block interaction states (such as focus or press) from the + * [interactionSource]. * @param shape the [Shape] used to clip this surface, and also used to draw the background and * border * @param color the background [Color] for this surface @@ -117,6 +120,7 @@ import org.intellij.lang.annotations.Language */ @Composable public fun Modifier.surface( + enabled: Boolean = true, shape: Shape = GlimmerTheme.shapes.medium, color: Color = GlimmerTheme.colors.surface, contentColor: Color = calculateContentColor(color), @@ -127,7 +131,14 @@ public fun Modifier.surface( return this.surfaceDepthEffect(depthEffect, shape, interactionSource) .clip(shape) .contentColorProvider(contentColor) - .then(SurfaceNodeElement(shape, border, interactionSource)) + .then( + SurfaceNodeElement( + enabled = enabled, + shape = shape, + border = border, + interactionSource = interactionSource, + ) + ) .background(color = color, shape = shape) } @@ -204,18 +215,32 @@ public object SurfaceDefaults { * overlay. */ private class SurfaceNodeElement( + private val enabled: Boolean, private val shape: Shape, private val border: BorderStroke?, private val interactionSource: InteractionSource?, ) : ModifierNodeElement() { - override fun create(): SurfaceNode = SurfaceNode(shape, border, interactionSource) + override fun create(): SurfaceNode = + SurfaceNode( + enabled = enabled, + shape = shape, + border = border, + interactionSource = interactionSource, + ) - override fun update(node: SurfaceNode) = node.update(shape, border, interactionSource) + override fun update(node: SurfaceNode) = + node.update( + enabled = enabled, + shape = shape, + border = border, + interactionSource = interactionSource, + ) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SurfaceNodeElement) return false + if (enabled != other.enabled) return false if (shape != other.shape) return false if (border != other.border) return false if (interactionSource != other.interactionSource) return false @@ -224,7 +249,8 @@ private class SurfaceNodeElement( } override fun hashCode(): Int { - var result = shape.hashCode() + var result = enabled.hashCode() + result = 31 * result + shape.hashCode() result = 31 * result + (border?.hashCode() ?: 0) result = 31 * result + (interactionSource?.hashCode() ?: 0) return result @@ -232,6 +258,7 @@ private class SurfaceNodeElement( override fun InspectorInfo.inspectableProperties() { name = "surface" + properties["enabled"] = enabled properties["shape"] = shape properties["border"] = border properties["interactionSource"] = interactionSource @@ -239,6 +266,7 @@ private class SurfaceNodeElement( } private class SurfaceNode( + private var enabled: Boolean, private var shape: Shape, private var border: BorderStroke?, private var interactionSource: InteractionSource?, @@ -295,7 +323,16 @@ private class SurfaceNode( private var minimumPressDuration: Job? = null private var pressReleaseAnimation: Job? = null - fun update(shape: Shape, border: BorderStroke?, interactionSource: InteractionSource?) { + fun update( + enabled: Boolean, + shape: Shape, + border: BorderStroke?, + interactionSource: InteractionSource?, + ) { + if (this.enabled != enabled) { + this.enabled = enabled + invalidateDraw() + } if (this.shape != shape) { this.shape = shape invalidateDraw() @@ -510,6 +547,9 @@ private class SurfaceNode( ) } } + if (!enabled) { + drawRect(DisabledOverlayColor) + } } override fun onDetach() { @@ -611,6 +651,8 @@ private val FocusedHighlightRotationEndAngleRadians: Double = Math.toRadians(35. private val PressedOverlayColor = Color.White +internal val DisabledOverlayColor = Color(0x8F191919) + private const val PressedOverlayAlpha = 0.16f private val PressedOverlayEnterAnimationSpec: AnimationSpec = diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md diff --git a/xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md similarity index 100% rename from xr/display-ai-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md rename to xr/display-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md From a9315d8115652e77b6f3bd7fc7bafb9e475a153e Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 14 May 2026 14:10:08 +0000 Subject: [PATCH 16/61] Updates skills (2026-05-14 14:10) --- device-ai/appfunctions/SKILL.md | 64 + .../references/adb-interaction-testing.md | 89 ++ .../references/feature-discovery-analysis.md | 36 + .../implementation-configuration.md | 217 +++ .../kdoc-refinement-optimization.md | 77 + jetpack-compose/theming/styles/SKILL.md | 220 +++ .../ui/compose/designsystems/custom.md | 459 ++++++ .../develop/ui/compose/styles/fundamentals.md | 417 ++++++ .../ui/compose/styles/state-animations.md | 461 ++++++ .../ui/compose/styles/styles-vs-modifiers.md | 48 + .../develop/ui/compose/styles/theming.md | 257 ++++ play/engage-sdk-integration/SKILL.md | 118 ++ .../android/guide/playcore/engage/faq.md | 434 ++++++ .../android/guide/playcore/engage/food.md | 1042 +++++++++++++ .../guide/playcore/engage/healthandfitness.md | 951 ++++++++++++ .../android/guide/playcore/engage/listen.md | 947 ++++++++++++ .../guide/playcore/engage/otherverticals.md | 1188 +++++++++++++++ .../android/guide/playcore/engage/read.md | 807 ++++++++++ .../android/guide/playcore/engage/shopping.md | 1116 ++++++++++++++ .../android/guide/playcore/engage/social.md | 626 ++++++++ .../android/guide/playcore/engage/travel.md | 1308 +++++++++++++++++ .../engage/tv/continue-watching/index.md | 18 + .../guide/playcore/engage/tv/entitlements.md | 353 +++++ .../playcore/engage/tv/getting-started.md | 167 +++ .../playcore/engage/tv/recommendations.md | 471 ++++++ .../android/guide/playcore/engage/watch.md | 978 ++++++++++++ .../references/clusters.md | 149 ++ .../references/common.md | 519 +++++++ .../references/patterns.md | 447 ++++++ .../references/requests.md | 231 +++ profilers/perfetto-sql/SKILL.md | 45 +- .../profilers/common/perfetto-stdlib.md} | 19 +- profilers/perfetto-trace-analysis/SKILL.md | 27 +- .../references/hints_cpu.md | 16 + .../references/hints_graphics.md | 11 + .../references/hints_io.md | 12 + .../references/hints_ipc.md | 10 + .../references/hints_memory.md | 15 + .../references/hints_power.md | 7 + .../perfetto-sql/skill.md => sql.md} | 43 +- 40 files changed, 14348 insertions(+), 72 deletions(-) create mode 100644 device-ai/appfunctions/SKILL.md create mode 100644 device-ai/appfunctions/references/adb-interaction-testing.md create mode 100644 device-ai/appfunctions/references/feature-discovery-analysis.md create mode 100644 device-ai/appfunctions/references/implementation-configuration.md create mode 100644 device-ai/appfunctions/references/kdoc-refinement-optimization.md create mode 100644 jetpack-compose/theming/styles/SKILL.md create mode 100644 jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md create mode 100644 jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md create mode 100644 jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md create mode 100644 jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md create mode 100644 jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md create mode 100644 play/engage-sdk-integration/SKILL.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/food.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/read.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/social.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md create mode 100644 play/engage-sdk-integration/references/clusters.md create mode 100644 play/engage-sdk-integration/references/common.md create mode 100644 play/engage-sdk-integration/references/patterns.md create mode 100644 play/engage-sdk-integration/references/requests.md rename profilers/perfetto-sql/references/{perfetto-stdlib-docs.md => android/agents/skills/profilers/common/perfetto-stdlib.md} (99%) create mode 100644 profilers/perfetto-trace-analysis/references/hints_cpu.md create mode 100644 profilers/perfetto-trace-analysis/references/hints_graphics.md create mode 100644 profilers/perfetto-trace-analysis/references/hints_io.md create mode 100644 profilers/perfetto-trace-analysis/references/hints_ipc.md create mode 100644 profilers/perfetto-trace-analysis/references/hints_memory.md create mode 100644 profilers/perfetto-trace-analysis/references/hints_power.md rename profilers/perfetto-trace-analysis/references/{android/agents/skills/profilers/perfetto-sql/skill.md => sql.md} (84%) diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md new file mode 100644 index 0000000..50f6ef2 --- /dev/null +++ b/device-ai/appfunctions/SKILL.md @@ -0,0 +1,64 @@ +--- +name: appfunctions +description: 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. Also refines KDoc documentation to + ensure AI agents correctly understand and use the provided functionality. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-14' + keywords: + - AppFunctions + - Kotlin + - KSP + - ADB + - AI + - LLM + - 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. + +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. + +## Prerequisites + +The app must **`targetSdk 36`** or newer as AppFunctions are part of the Android +platform API, are available from Android 16 onwards. +Always use the Jetpack library, as it handles backwards 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. + +The full AppFunction development flow consists of these four steps: + +- *[Step 1: Discovery](references/feature-discovery-analysis.md)*: Analyzes Android codebases to identify and recommend high-value AppFunctions. Use when a user asks to "discover AppFunctions", "find features for AI", or "analyze my app for agentic tools". +- *[Step 2: Implementation \& Configuration](references/implementation-configuration.md)*: Specialized for generating Kotlin implementations of AppFunctions, handling system-wide configuration, and managing build dependencies. Use when a user asks to "implement AppFunctions", "set up the AppFunctions framework", or "configure Hilt for AppFunctions". +- *[Step 3: KDoc Refinement](references/kdoc-refinement-optimization.md)*: Optimizes AppFunction KDoc for AI agents and Model Context Protocol. Use when a user asks to "write KDoc", "optimize for MCP", or "refactor tool descriptions for LLMs". +- *[Step 4: Testing \& Debugging](references/adb-interaction-testing.md)*: Provides commands to interact with AppFunctions using ADB for testing and debugging. Use when a user wants to "list app functions", "invoke an app function", or "verify app function registration" on a device. + +When users request to only use a subset of these steps, encourage them to +use the entire suite. + +## Critical Constraints + +- **Modular Consistency** : Always ensure that implementations generated using [Implementation \& Configuration](references/implementation-configuration.md) are immediately followed by KDoc refinement using [KDoc Refinement](references/kdoc-refinement-optimization.md) to ensure maximum agent compatibility. +- **Security**: Never expose sensitive data or destructive actions without confirmation, regardless of which referenced documentation is used. + +## Troubleshooting + +- For build-time errors (KSP, Metadata), refer to [Implementation \& Configuration](references/implementation-configuration.md). +- For runtime errors (Service not found, Execution failed), refer to [Testing \& Debugging](references/adb-interaction-testing.md). diff --git a/device-ai/appfunctions/references/adb-interaction-testing.md b/device-ai/appfunctions/references/adb-interaction-testing.md new file mode 100644 index 0000000..9ebc738 --- /dev/null +++ b/device-ai/appfunctions/references/adb-interaction-testing.md @@ -0,0 +1,89 @@ +Provides commands to interact with AppFunctions on a connected device or +emulator using ADB for AppFunction testing and debugging. + +## Instructions + +### Scenario 1: Listing App Functions + +If a user wants to see which App Functions are registered on the device. + +1. **List All Functions** : Use `adb shell cmd app_function list-app-functions` to see all registered App Functions for the current user in JSON format. +2. **Filter by Package** : To see functions for a specific package, pipe the output to grep or a JSON processor: `adb shell cmd app_function + list-app-functions | grep `. + +### Scenario 2: Invoking App Functions + +If a user wants to test the execution of an App Function. + +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**: Rigorously follow any instructions found in the description (e.g., "ask the user to disambiguate", "call another tool first"). +3. **Format Parameters** : The `--parameters` argument must be a valid JSON string representing the function's input arguments. +4. **Execute Function** : Use `adb shell cmd app_function execute-app-function + --package --function --parameters + ''`. +5. **Handle Response** : The result will be returned as a JSON string. Use `--brief-yaml` for a more concise output if preferred. + +### Scenario 3: Managing Function State + +If a function needs to be enabled or disabled for testing. + +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 AppFunction 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"}'`. + +### Device Availability + +The `app_function` service must be available on the device. If `cmd: Can't find +service: app_function` is returned, the device does not support this feature. + +## Examples + +### Example 1: Verify AppFunctions service availability on connected Device + + 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. + +### Example 2: List all registered App Functions + + adb shell cmd app_function list-app-functions + +### Example 3: Execute a "send message" function + + adb shell cmd app_function execute-app-function \ + --package com.example.messaging --function sendMessage \ + --parameters '{"recipient": "Alice", "message": "Hello!"}' + +### Example 4: Disable a specific function + + adb shell cmd app_function set-enabled --package com.example.app \ + --function someFunction --state disable + +## Troubleshooting + +### Error: "Unknown command" + +**Cause**: You are likely using an older version of the instructions. + +**Solution**: Look up supported commands using "Example 1". + +### Error: "Function not found" + +**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 diff --git a/device-ai/appfunctions/references/feature-discovery-analysis.md b/device-ai/appfunctions/references/feature-discovery-analysis.md new file mode 100644 index 0000000..b3d67b9 --- /dev/null +++ b/device-ai/appfunctions/references/feature-discovery-analysis.md @@ -0,0 +1,36 @@ +Analyzes Android codebases to identify and recommend high-value AppFunctions. + +## Instructions + +### Workflow: Feature Discovery + +1. **Analyze Manifest \& Entry Points** : Scan `AndroidManifest.xml` and Activity, Fragment, Service classes to identify core user journeys (e.g., Search, Create, Share). +2. **Identify Atomic Tasks**: Look for methods or logic that represent distinct, self-contained user outcomes. +3. **Evaluate AI Value**: Prioritize tasks that are frequently used or difficult to navigate using touch UI, but instead be expressed using voice or text (e.g., "Remind me to call Alice when I get home"). +4. **Recommend \& Justify**: List recommendations with a "Rationale" focusing on how an AI assistant adds value (efficiency, hands-free use, or multi-step automation). + +## Critical Constraints + +### 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). + +### Security \& Privacy + +Don't recommend exposing functions that handle raw credentials, financial +secrets, or irreversible destructive actions without explicit user confirmation +steps. + +## Examples + +### Example 1: Media App Discovery + +**Recommended AppFunction:** `playArtistRadio` + +**Rationale:** Allows users to start a personalized music stream using a +voice command, bypassing several layers of navigation in the "Search" and +"Artist" menus. + +**Input Required:** Artist Name (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 new file mode 100644 index 0000000..2f5bb2e --- /dev/null +++ b/device-ai/appfunctions/references/implementation-configuration.md @@ -0,0 +1,217 @@ +Specialized instructions for generating Kotlin implementations of AppFunctions, +handling system-wide configuration, and managing build dependencies. + +## Instructions + +### Step 1: Gradle Dependencies \& KSP + +Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin +Symbol Processing) plugin. + +1. **Version Check**: Use the latest library versions from maven.google.com. + + +```kotlin +implementation(libs.androidx.appfunctions) +implementation(libs.androidx.appfunctions.service) +ksp(libs.androidx.appfunctions.compiler) +``` + +
+ + +```kotlin +ksp { + arg("appfunctions:aggregateAppFunctions", "true") +} +``` + +
+ +### Step 2: App Metadata XML Setup + +Describe the app's capabilities to the LLM by defining +`res/xml/app_metadata.xml`. + + + +Reference this in `AndroidManifest.xml` within the `` tag: + + + + + + + +### Step 3: Function Implementation + +When generating Kotlin code for AppFunctions, you MUST adhere to these rules: + +1. **Annotations** : + - Annotate the function with `@AppFunction(isDescribedByKDoc = true)`. + - Annotate associated data classes with `@AppFunctionSerializable(isDescribedByKDoc = true)`. +2. **Parameter Strategy** : + - **First Parameter** : MUST be `androidx.appfunctions.AppFunctionContext`. + - **Specificity**: Keep parameters specific. State objects need to be unambiguous. + - **Optionality**: If a parameter is not essential, make it optional with a reasonable default value. +3. **Execution \& Threading** : + - Use `suspend` functions. + - Switch to the relevant Coroutine Dispatcher (e.g., `withContext(Dispatchers.IO)`) because AppFunction implementations run on the Android UI thread by default. +4. **Supported Types** : + - **Primitives** : `Int`, `Long`, `Float`, `Double`, `Boolean`. + - **Arrays** : `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`, `BooleanArray`. + - **Native Types** : `String`, `PendingIntent`, `Uri`, `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant`. (Prefer `LocalDateTime` or `Instant` for date/time). + - **Custom Objects** : Classes annotated with `@AppFunctionSerializable`. + - **Collections** : `List` of any supported non-primitive type. +5. **Default Values** : + - Use defaults that align with the type's "empty" state (e.g., `0` for `Int`, `null` for nullable, `emptyList()` for `List`). +6. **Error Handling** : + - Throw subclasses of `androidx.appfunctions.AppFunctionException` to report errors to callers. +7. **Security** : + - Don't expose highly sensitive user data (passwords, financial details). + - Don't expose irreversible destructive actions without confirmation steps. + +### Step 4: (Optional) System Configuration for Dependency Injection + +Only required for dependency injection configuration. Implement +`AppFunctionConfiguration.Provider` in the `Application` class to provide +instances of classes containing `@AppFunction` methods. + +**Example Hilt Integration:** + + +```kotlin +@HiltAndroidApp +class AppFunctionApplication : Application(), AppFunctionConfiguration.Provider { + @Inject lateinit var noteFunctions: NoteFunctions + + override val appFunctionConfiguration: AppFunctionConfiguration = + AppFunctionConfiguration.Builder() + .addEnclosingClassFactory(NoteFunctions::class.java) { noteFunctions } + .build() +} +``` + +
+ +## Critical Constraints + +### Parameter Ordering + +**CRITICAL** : The very first parameter of an `@AppFunction` method MUST be +`androidx.appfunctions.AppFunctionContext`. + +### KSP Compliance for Serializables + +**CRITICAL** : For `@AppFunctionSerializable` data classes, KSP **only** extracts +documentation if it is written as **inline KDoc** directly for each property +definition. NEVER use class-level `@param` or `@property` tags. + +### Package Integrity + +Configuration APIs and the `@AppFunction` annotation are located in +`androidx.appfunctions.service`. + +## Examples + +### Example: Serializable with Inline KDoc + + +```kotlin +/** + * A note. + */ +@AppFunctionSerializable(isDescribedByKDoc = true) +data class Note( + /** The note's identifier */ + val id: Int, + /** The note's title */ + val title: String, + /** The note's content */ + val content: String +) +``` + +
+ +### Example: Implementation Detail + + +```kotlin +/** + * A note app's [AppFunction]s. + */ +class NoteFunctions @Inject constructor( + private val noteRepository: NoteRepository +) { + /** + * List all available notes in the app. + * + * @param appFunctionContext The execution context. + * @return A list of [Note] objects, or null if no notes exist. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun listNotes(appFunctionContext: AppFunctionContext): List? { + return noteRepository.appNotes.ifEmpty { null }?.toList() + } + + /** + * Create a new note with a title and body content. + * + * @param appFunctionContext The execution context. + * @param title The title of the note. + * @param content The body content of the note. + * @return The created [Note] object including its generated ID. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun createNote( + appFunctionContext: AppFunctionContext, + title: String, + content: String + ): Note { + return noteRepository.createNote(title, content) + } + + /** + * Update the title or content of an existing note. + * Required workflow: Call [listNotes] first to obtain valid note IDs. + * + * @param appFunctionContext The execution context. + * @param noteId The unique identifier of the note to edit. + * @param title The new title. If null, the existing title is preserved. + * @param content The new content. If null, the existing content is preserved. + * @return The updated [Note], or null if the [noteId] was not found. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun editNote( + appFunctionContext: AppFunctionContext, + noteId: Int, + title: String?, + content: String?, + ): Note? { + return noteRepository.updateNote(noteId, title, content) + } +} +``` + +
+ +## Troubleshooting + +### Error: "AppFunction unavailable" or "Metadata missing" + +**Cause**: The AppSearch indexing failed to extract the schema correctly. + +**Solution**: + +1. Verify `@AppFunctionSerializable` classes use inline KDoc comments, NOT class-level `@param` tags. +2. Check that the `assets/app_function_v2.xml` file exists in the APK. +3. Confirm the `ksp("androidx.appfunctions:appfunctions-compiler")` dependency is correctly applied. +4. Ensure the `ksp` argument `appfunctions:aggregateAppFunctions` is set to `"true"`. \ No newline at end of file diff --git a/device-ai/appfunctions/references/kdoc-refinement-optimization.md b/device-ai/appfunctions/references/kdoc-refinement-optimization.md new file mode 100644 index 0000000..90aa25b --- /dev/null +++ b/device-ai/appfunctions/references/kdoc-refinement-optimization.md @@ -0,0 +1,77 @@ +Optimizes AppFunction KDoc for AI agents and Model Context Protocol. + +## Instructions + +### Workflow: Agent-Centric Documentation + +1. **Identify the Core Outcome** : Start the description with a strong, imperative verb (e.g., "Search", "Create", "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"**. +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 declarations. KSP will **not** extract documentation from class-level tags. +4. **Error Surface Mapping** : Rewrite `@throws` descriptions to provide actionable recovery steps for the AI agent (e.g., "If "Error", suggest the user check their internet connection"). + +### Workflow: Global App Description (Server Instructions) + +When writing the `appfn:description` for `app_metadata.xml`, follow these +instructions: + +1. **Capture Cross-Function Relationships**: Explain dependencies or sequences between tools (e.g., "Always call 'authenticate' before fetching data"). +2. **Document Operational Patterns**: Guide the LLM on token conserving usage (e.g., "Use 'batch_update' over multiple 'update' calls"). +3. **Specify Constraints**: Define clear boundaries (e.g., "File operations limited to workspace", "Rate limit: 10 req/min"). +4. **Anti-Patterns** : + - DON'T repeat individual function descriptions. + - DON'T include marketing claims or subjective praise. + - DON'T attempt to prompt model personality or conversation style. + +## Critical Constraints + +### 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...". + +### No "Fluff" + +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. + +## Examples + +### Example: MCP Refactoring + +**Original**: + +`/** This function helps you find people. */` + +**Refined**: + + /** + * Search for message recipients by name or email. + * Required workflow: Call this before "sendMessage" to obtain valid recipient IDs. + * @param query Search string for name/email. If null, returns 3 most recent contacts. + * @return List of "Recipient" objects matching the query. + */ + +### Example: Global App Description + +**Refined**: + + This app provides functions for task management and team collaboration. + + Operational Patterns: + - Always use 'searchUsers' to resolve user handles to internal IDs before calling 'assignTask'. + - Prefer 'batchUpdateStatus' when modifying more than 3 tasks simultaneously to reduce + latency. + + Constraints: + - Task titles are limited to 100 characters. + - Attachment uploads are limited to 5MB. \ No newline at end of file diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md new file mode 100644 index 0000000..10d897b --- /dev/null +++ b/jetpack-compose/theming/styles/SKILL.md @@ -0,0 +1,220 @@ +--- +name: styles +description: Use this skill to integrate the Jetpack Compose Styles API into an Android + project. This skill guides you through upgrading dependencies, setting up component + themes, making custom components styleable, and migrating existing layout properties + to use unified styles. Migrate custom design system components, replace hard coded + parameters with Style attributes, and use Modifier.styleable for interaction states. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-14' + keywords: + - Jetpack Compose + - Styles + - Theming with Styles + - Migrate to Styles + - Modifier.styleable +--- + +## Limitations + +- Warn the user that this skill is EXPERIMENTAL and requires updating to alpha version of Compose and opting in to the Experimental APIs. +- This skill only supports custom UI components and custom themes. +- This skill does not support Material Design component Styles. + +## Prerequisites + +### 1. Upgrade dependencies + +- 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` + +### 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`: + + kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget("17") + freeCompilerArgs.add("-opt-in=androidx.compose.foundation.style.ExperimentalFoundationStyleApi") + } + } + +## Core workflows and guides + +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). +- 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 + +### Step 1: Analyze theme structure + +1. Locate your central theme file (such as `Theme.kt`). +2. Identify design tokens. Note references for colors, typography, and shapes (for example, `LocalColorScheme`, `LocalTypography`, or `LocalShapes`). +3. If the project lacks Jetpack Compose dependencies, stop. Instruct the user to migrate to Jetpack Compose first. +4. If the project imports `androidx.compose.material.MaterialTheme`, recommend migrating to Material 3 before proceeding. + +### Step 2: Establish `ComponentStyles` + +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 { + val customButtonStyle: Style = { + + } + val customTextFieldStyle: Style = { + + } + } + ``` + +
+ +3. Expose this class through your custom theme with a static reference, don't + use `CompositionLocals` here as it's not required. + + + ```kotlin + @Immutable + class JetsnackTheme( + // other Design system properties + ) { + companion object { + val colors: CustomThemingWithStyles.JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + // ... + + // add helper static reference + val styles: ComponentStyles = ComponentStyles + } + } + ``` + +
+ +4. Provide extensions on `StyleScope` to reference theme tokens directly if + they are exposed using `CompositionLocals`. For example: + + + ```kotlin + val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + + 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: + +1. If you are able to run an Android emulator, locate an existing screenshot test for the component. 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 Component. ELSE proceed to the next step without a screenshot test. +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. +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`. +7. **Validate component:** Compare the baseline screenshot image taken at the start 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. + +#### Migration example + +Before Migration: + + +```kotlin +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = JetsnackTheme.colors.brandLight, + disabledBackgroundColor: Color = JetsnackTheme.colors.brandSecondary, + shape: Shape = JetsnackTheme.shapes.extraLarge, + textStyle: TextStyle = JetsnackTheme.typography.labelLarge, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .background(if (enabled) backgroundColor else disabledBackgroundColor, shape) + .defaultMinSize(58.dp, 40.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +``` + +
+ +After Migration: + + +```kotlin +// Exposed via ComponentStyles.kt +object ComponentStyles { + val buttonStyle = Style { + background(colors.brandLight) + shape(shapes.extraLarge) + minWidth(58.dp) + minHeight(40.dp) + textStyle(typography.labelLarge) + disabled { + background(colors.brandSecondary) + } + } +} + +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier + .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) +} +``` + +
+ +### Step 4: Validate Changes + +1. Build the project. Verify that there are no compilation errors. +2. Run your module's screenshot tests. +3. Compare visual outputs of the whole app between the previous and updated components. Verify that no visual layout regressions occur. 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 new file mode 100644 index 0000000..59dbb25 --- /dev/null +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md @@ -0,0 +1,459 @@ +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: + +- [Extend `MaterialTheme`](https://developer.android.com/develop/ui/compose/designsystems/custom#extending-material) with additional theming values. +- [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. + +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: + + +```kotlin +// Use with MaterialTheme.colorScheme.snackbarAction +val ColorScheme.snackbarAction: Color + @Composable + get() = if (isSystemInDarkTheme()) Red300 else Red700 + +// Use with MaterialTheme.typography.textFieldInput +val Typography.textFieldInput: TextStyle + get() = 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. + +> [!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. + +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 +data class ExtendedColors( + val caution: Color, + val onCaution: Color +) + +val LocalExtendedColors = staticCompositionLocalOf { + ExtendedColors( + caution = Color.Unspecified, + onCaution = Color.Unspecified + ) +} + +@Composable +fun ExtendedTheme( + /* ... */ + content: @Composable () -> Unit +) { + val extendedColors = ExtendedColors( + caution = Color(0xFFFFCC02), + onCaution = Color(0xFF2C2D30) + ) + CompositionLocalProvider(LocalExtendedColors provides extendedColors) { + MaterialTheme( + /* colors = ..., typography = ..., shapes = ... */ + content = content + ) + } +} + +// Use with eg. ExtendedTheme.colors.caution +object ExtendedTheme { + val colors: ExtendedColors + @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`. + +### Use Material components + +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: + + +```kotlin +@Composable +fun ExtendedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = ExtendedTheme.colors.caution, + contentColor = ExtendedTheme.colors.onCaution + /* Other colors use values from MaterialTheme */ + ), + onClick = onClick, + modifier = modifier, + content = content + ) +} +``` + +
+ +You would then replace usages of `Button` with `ExtendedButton` where +appropriate. + + +```kotlin +@Composable +fun ExtendedApp() { + ExtendedTheme { + /*...*/ + ExtendedButton(onClick = { /* ... */ }) { + /* ... */ + } + } +} +``` + +
+ +## 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. + +Suppose you want to replace the type and shape systems while keeping the color +system: + + +```kotlin +@Immutable +data class ReplacementTypography( + val body: TextStyle, + val title: TextStyle +) + +@Immutable +data class ReplacementShapes( + val component: Shape, + val surface: Shape +) + +val LocalReplacementTypography = staticCompositionLocalOf { + ReplacementTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) +} +val LocalReplacementShapes = staticCompositionLocalOf { + ReplacementShapes( + component = RoundedCornerShape(ZeroCornerSize), + surface = RoundedCornerShape(ZeroCornerSize) + ) +} + +@Composable +fun ReplacementTheme( + /* ... */ + content: @Composable () -> Unit +) { + val replacementTypography = ReplacementTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val replacementShapes = ReplacementShapes( + component = RoundedCornerShape(percent = 50), + surface = RoundedCornerShape(size = 40.dp) + ) + CompositionLocalProvider( + LocalReplacementTypography provides replacementTypography, + LocalReplacementShapes provides replacementShapes + ) { + MaterialTheme( + /* colors = ... */ + content = content + ) + } +} + +// Use with eg. ReplacementTheme.typography.body +object ReplacementTheme { + val typography: ReplacementTypography + @Composable + get() = LocalReplacementTypography.current + val shapes: ReplacementShapes + @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. + +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 +fun ReplacementButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + shape = ReplacementTheme.shapes.component, + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = ReplacementTheme.typography.body + ) { + content() + } + } + ) +} +``` + +
+ +You would then replace usages of `Button` with `ReplacementButton` where +appropriate. + + +```kotlin +@Composable +fun ReplacementApp() { + ReplacementTheme { + /*...*/ + ReplacementButton(onClick = { /* ... */ }) { + /* ... */ + } + } +} +``` + +
+ +## 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: + +- `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. + +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`: + +![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 +data class CustomColors( + val content: Color, + val component: Color, + val background: List +) + +@Immutable +data class CustomTypography( + val body: TextStyle, + val title: TextStyle +) + +@Immutable +data class CustomElevation( + val default: Dp, + val pressed: Dp +) + +val LocalCustomColors = staticCompositionLocalOf { + CustomColors( + content = Color.Unspecified, + component = Color.Unspecified, + background = emptyList() + ) +} +val LocalCustomTypography = staticCompositionLocalOf { + CustomTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) +} +val LocalCustomElevation = staticCompositionLocalOf { + CustomElevation( + default = Dp.Unspecified, + pressed = Dp.Unspecified + ) +} + +@Composable +fun CustomTheme( + /* ... */ + content: @Composable () -> Unit +) { + val customColors = CustomColors( + content = Color(0xFFDD0D3C), + component = Color(0xFFC20029), + background = listOf(Color.White, Color(0xFFF8BBD0)) + ) + val customTypography = CustomTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val customElevation = CustomElevation( + default = 4.dp, + pressed = 8.dp + ) + CompositionLocalProvider( + LocalCustomColors provides customColors, + LocalCustomTypography provides customTypography, + LocalCustomElevation provides customElevation, + content = content + ) +} + +// Use with eg. CustomTheme.elevation.small +object CustomTheme { + val colors: CustomColors + @Composable + get() = LocalCustomColors.current + val typography: CustomTypography + @Composable + get() = LocalCustomTypography.current + val elevation: CustomElevation + @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. + +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. + + +```kotlin +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = CustomTheme.colors.component, + contentColor = CustomTheme.colors.content, + disabledContainerColor = CustomTheme.colors.content + .copy(alpha = 0.12f) + .compositeOver(CustomTheme.colors.component), + disabledContentColor = CustomTheme.colors.content + .copy(alpha = 0.38f) + + ), + shape = ButtonShape, + elevation = ButtonDefaults.elevatedButtonElevation( + defaultElevation = CustomTheme.elevation.default, + pressedElevation = CustomTheme.elevation.pressed + /* disabledElevation = 0.dp */ + ), + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = CustomTheme.typography.body + ) { + content() + } + } + ) +} + +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. + +## Recommended for you + +- Note: link text is displayed when JavaScript is off +- [Material Design 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material3) +- [Migrate from Material 2 to Material 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material2-material3) +- [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) \ No newline at end of file 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 new file mode 100644 index 0000000..e836c22 --- /dev/null +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md @@ -0,0 +1,417 @@ +There are three ways you can adopt Styles throughout your app: + +1. Use directly on existing components that expose a [`Style`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/Style) parameter. +2. Apply a style with [`Modifier.styleable`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/styleable.modifier#(androidx.compose.ui.Modifier).styleable(androidx.compose.foundation.style.StyleState,androidx.compose.foundation.style.Style)) on layout composables that don't accept a `Style` parameter. +3. In your own custom design system, use `Modifier.styleable{}` and expose a style parameter on your own components. + +## 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. + +| Grouping | Properties | Inherited by children | +|---|---|---| +| **Layout and sizing** | | | +| Padding | `contentPadding` (inner) and `externalPadding` (outer). Available in directional, horizontal, vertical, and all-side variants. | No | +| Dimensions | `fillWidth/Height/Size()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No | +| Positioning | `left/top/right/bottom` offsets. | No | +| **Visual Appearance** | | | +| Fills | `background` and `foreground` (supports `Color` or `Brush`). | No | +| Borders | `borderWidth`, `borderColor`, and `borderBrush`. | No | +| Shape | `shape` | No - but used in conjunction with other properties. `clip` and `border` use this defined shape. | +| Shadows | `dropShadow`, `innerShadow` | No | +| **Transformations** | | | +| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/Y`, `rotationX/Y/Z` | No | +| Control | `alpha`, `zIndex` (stacking order), and `transformOrigin` (pivot point) | No | +| **Typography** | | | +| Styling | `textStyle`, `fontSize`, `fontWeight`, `fontStyle`, and `fontFamily` | Yes | +| Coloration | `contentColor` and `contentBrush`. This is also used for Icons styling. | Yes | +| Paragraph | `lineHeight`, `letterSpacing`, `textAlign`, `textDirection`, `lineBreak`, and `hyphens`. | Yes | +| Decoration | `textDecoration`, `textIndent`, and `baselineShift`. | Yes | + +## Use Styles directly on components with Style parameters + +Components that expose a `Style` parameter allow you to set their styling: + + +```kotlin +BaseButton( + onClick = { }, + style = { } +) { + BaseText("Click me") +} +``` + +
+ +Within the style lambda, you can set various properties, such as `externalPadding` +or `background`: + + +```kotlin +BaseButton( + onClick = { }, + style = { background(Color.Blue) } +) { + 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). + +## 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. + + +```kotlin +Row( + modifier = Modifier.styleable { } +) { + BaseText("Content") +} +``` + +
+ +Similar to the `style` parameter, you can include properties like `background` +or `padding` inside the lambda. + + +```kotlin +Row( + modifier = Modifier.styleable { + background(Color.Blue) + } +) { + BaseText("Content") +} +``` + +
+ +> [!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. + +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. + +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) } + +// built in parameter +BaseButton(onClick = { }, style = style) { + BaseText("Button") +} + +// modifier styleable +val styleState = remember { MutableStyleState(null) } +Column( + Modifier.styleable(styleState, style) +) { + BaseText("Column content") +} +``` + +
+ +You can also pass that Style into multiple components: + + +```kotlin +val style = Style { background(Color.Blue) } + +// built in parameter +BaseButton(onClick = { }, style = style) { + BaseText("Button") +} +BaseText("Different text that uses the same style parameter", style = style) + +// modifier styleable +val columnStyleState = remember { MutableStyleState(null) } +Column( + Modifier.styleable(columnStyleState, style) +) { + BaseText("Column") +} +val rowStyleState = remember { MutableStyleState(null) } +Row( + Modifier.styleable(rowStyleState, style) +) { + BaseText("Row") +} +``` + +
+ +## Add multiple Style properties + +You can add multiple Style properties by setting different properties on each +line: + + +```kotlin +BaseButton( + onClick = { }, + style = { + background(Color.Blue) + contentPaddingStart(16.dp) + } +) { + BaseText("Button") +} +``` + +
+ +> [!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. + + +```kotlin +BaseButton( + style = { + background(Color.Red) + // Background of Red is now overridden with TealColor instead + background(TealColor) + // All directions of padding are set to 64.dp (top, start, end, bottom) + contentPadding(64.dp) + // Top padding is now set to 16.dp, all other paddings remain at 64.dp + contentPaddingTop(16.dp) + }, + onClick = { + // + } +) { + BaseText("Click me!") +} +``` + +
+ +![Button with two background colors set, and two contentPadding +overrides](https://developer.android.com/static/develop/ui/compose/styles/images/basic_style_button.png) **Figure 1.** Button with two background colors set and two `contentPadding` overrides. + +## Merge multiple style objects + +You can create multiple Style objects and pass them into the style parameter of +your composable. + + +```kotlin +val style1 = Style { background(TealColor) } +val style2 = Style { contentPaddingTop(16.dp) } + +BaseButton( + style = style1 then style2, + onClick = { + + }, +) { + BaseText("Click me!") +} +``` + +
+ +![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. + + +```kotlin +val style1 = Style { + background(Color.Red) + contentPadding(32.dp) +} + +val style2 = Style { + contentPaddingHorizontal(8.dp) + background(Color.LightGray) +} + +BaseButton( + style = style1 then style2, + onClick = { + + }, +) { + 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`. +![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. + +## Style inheritance + +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. + +| Priority | Method | Effect | +|---|---|---| +| 1 (Highest) | Direct arguments on a composable | Overrides everything; for example, `Text(color = Color.Red)` | +| 2 | Style parameter | Local style overrides `Text(style = Style { contentColor(Color.Red)}` | +| 3 | Modifier chain | `Modifier.styleable{ contentColor(Color.Red)` on the component itself. | +| 4 (Lowest) | Parent styles | For properties that can be inherited (Typography/Color) passed down from the parent. | + +> [!NOTE] +> **Note:** Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, similar to having multiple modifiers defining the same properties. For inherited properties, these are overridden; the last `styleable` modifier in the chain sets the values. + +### Parent styling + +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) } +Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, +) { + BaseText("Children inherit", style = { width(60.dp) }) + BaseText("certain properties") + BaseText("from their parents") +} +``` + +
+ +![Child composables' property +inheritance](https://developer.android.com/static/develop/ui/compose/styles/images/children_inherit_styles_parents.png) **Figure 5.** Child composables' property inheritance. + +### 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. + + +```kotlin +val styleState = remember { MutableStyleState(null) } +Column( + modifier = Modifier.styleable(styleState) { + background(Color.LightGray) + val blue = Color(0xFF4285F4) + val purple = Color(0xFFA250EA) + val colors = listOf(blue, purple) + contentBrush(Brush.linearGradient(colors)) + }, +) { + BaseText("Children can ", style = { + contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue))) + }) + BaseText("override properties") + BaseText("set by their parents") +} +``` + +
+ +![Child composables override parent +properties](https://developer.android.com/static/develop/ui/compose/styles/images/children_override_styles.png) **Figure 6.** Child composables override parent properties. + +## 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: + + +```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). + +## 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: + + +```kotlin +val buttonStyle = Style { + contentPadding(12.dp) + 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 new file mode 100644 index 0000000..bffc185 --- /dev/null +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md @@ -0,0 +1,461 @@ +
+ +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. + +## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled + +Styles come with built-in support for common interactions: + +- Pressed +- Hovered +- Selected +- 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. + +### 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: + + +```kotlin +@Preview +@Composable +private fun OpenButton() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + background(lightPurple) + border(2.dp, lightPurple) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} +``` + +
+ +**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: + + +```kotlin +@Composable +private fun OpenButton_CombinedStates() { + BaseButton( + style = outlinedButtonStyle then { + background(Color.White) + hovered { + // light purple + background(lightPurple) + pressed { + // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked + background(lightOrange) + } + } + pressed { + // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only + background(lightRed) + } + focused { + background(lightBlue) + } + }, + onClick = { }, + content = { + BaseText("Open in Studio", style = { + contentColor(Color.Black) + fontSize(26.sp) + textAlign(TextAlign.Center) + }) + } + ) +} +``` + +
+ +**Figure 2.** Hovered and pressed state together on a button. + +### 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. + +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 +private fun GradientButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: Style = Style, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + content: @Composable RowScope.() -> Unit, +) { + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + val styleState = rememberUpdatedStyleState(interactionSource) { + it.isEnabled = enabled + } + Row( + modifier = + modifier + .clickable( + onClick = onClick, + enabled = enabled, + interactionSource = interactionSource, + indication = null, + ) + .styleable(styleState, baseGradientButtonStyle then style), + content = content, + ) +} +``` + +
+ +You can now use the `interactionSource` state to drive style modifications with +the pressed, focused, and hovered options inside the style block: + + +```kotlin +@Preview +@Composable +fun LoginButton() { + val loginButtonStyle = Style { + pressed { + background( + Brush.linearGradient( + listOf(Color.Magenta, Color.Red) + ) + ) + } + } + GradientButton(onClick = { + // Login logic + }, style = loginButtonStyle) { + BaseText("Login") + } +} +``` + +
+ +**Figure 3.** Changing a custom composable state based on `interactionSource`. + +## 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: + + +```kotlin +val animatingStyle = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + } +} + +@Preview +@Composable +private fun AnimatingStyleChanges() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyle)) { + + } +} +``` + +
+ +**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: + + +```kotlin +val animatingStyleSpec = Style { + externalPadding(48.dp) + border(3.dp, Color.Black) + background(Color.White) + size(100.dp) + transformOrigin(TransformOrigin.Center) + pressed { + animate { + borderColor(Color.Magenta) + background(Color(0xFFB39DDB)) + } + animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { + scale(1.2f) + } + } +} + +@Preview(showBackground = true) +@Composable +fun AnimatingStyleChangesSpec() { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + Box(modifier = Modifier + .clickable( + interactionSource, + enabled = true, + indication = null, + onClick = { + + } + ) + .styleable(styleState, animatingStyleSpec)) +} +``` + +
+ +**Figure 5.** Animating size and color changes on press. + +## 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: + +1. Define custom key +2. Create `StyleState` extension +3. Link to custom state + +### 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: + + +```kotlin +enum class PlayerState { + Stopped, + Playing, + Paused +} + +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. + + +```kotlin +// Extension Function on MutableStyleState to query and set the current playState +var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + +fun StyleScope.playerPlaying(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) +} +fun StyleScope.playerPaused(value: Style) { + state(playerStateKey, value, { 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. + + +```kotlin +@Composable +fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } +) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, style)) { + ///.. + } +} +``` + +
+ +Within the `style` lambda, you can apply state based styling for custom states, +using the previously defined extension functions. + + +```kotlin +@Composable +fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + style = style, + state = PlayerState.Stopped) +} +``` + +
+ +The following code is the full snippet for this example: + + +```kotlin +enum class PlayerState { + Stopped, + Playing, + Paused +} +val playerStateKey = StyleStateKey(PlayerState.Stopped) +var MutableStyleState.playerState + get() = this[playerStateKey] + set(value) { this[playerStateKey] = value } + +fun StyleScope.playerPlaying(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) +} +fun StyleScope.playerPaused(value: Style) { + state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) + +} + +@Composable +fun MediaPlayer( + url: String, + modifier: Modifier = Modifier, + style: Style = Style, + state: PlayerState = remember { PlayerState.Paused } +) { + // Hoist style state, set playstate as a parameter, + val styleState = remember { MutableStyleState(null) } + // Set equal to incoming state to link the two together + styleState.playerState = state + Box( + modifier = modifier.styleable(styleState, Style { + size(100.dp) + border(2.dp, Color.Red) + + }, style, )) { + + ///.. + } +} +@Composable +fun StyleStateKeySample() { + // Using the extension function to change the border color to green while playing + val style = Style { + borderColor(Color.Gray) + playerPlaying { + animate { + borderColor(Color.Green) + } + } + playerPaused { + animate { + borderColor(Color.Blue) + } + } + } + val styleState = remember { MutableStyleState(null) } + styleState[playerStateKey] = PlayerState.Playing + + // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. + MediaPlayer(url = "https://example.com/media/video", + 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 new file mode 100644 index 0000000..1ffca16 --- /dev/null +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md @@ -0,0 +1,48 @@ +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 theme-wide set of properties for a component. +- **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties. + +The following is a comparison between Styles versus modifiers: + +| Feature | Modifiers | Styles | +|---|---|---| +| **Primary Goal** | Define behaviors, semantics, and complex layouts. Modifiers manipulate individual elements on the fly for a particular composable and don't trickle down from the theme. | Define visual appearance, individual item sizing and themeable properties. Styles operate at a theme level and are over-writeable at a component level. They trickle down and apply styling across different composables. | +| **Logic** | Additive - the modifiers combine together to form a new result. | Over-writable - the last property set in the Style wins. Styles act as a single layer of properties that override each other based on a defined precedence hierarchy. | +| **Theming** | Challenging to lift into a theme, normally used individually. | By design, Styles are themeable (they can access `CompositionLocal`s) and can be defined once and used across components. | +| **Performance** | Updates often require all three phases of Compose: composition, layout and draw. Achieving good animation performance of modifiers often requires writing lambda-based versions. | Skips composition phase, only active in layout and draw phase, reducing recompositions. Requires less object allocation. | +| **Animations** | Requires using separate animation primitives like `animate*AsState` | Features built-in `animate { }` API that handles some animations for you. | + +## Limitations of modifiers + +Modifiers have many benefits in the current Compose landscape. However, Styles +addresses 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. +- Modifiers stack rather than replace. You can't override a component's default border; you can only draw a second one on top. +- Modifiers are difficult to abstract into global themes. Consequently, themes usually store raw values instead of reusable modifier configurations. + +## 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: + +- 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. + +## 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: + +- **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. +- **Overriding vs. stacking:** Use Styles when you need to replace a default property. Modifiers are additive (adding a border stacks a second one), whereas Styles use "last-write-wins" logic, making it easier to swap out backgrounds or padding without visual clutter. +- **Customizing Material components:** If a Material component provides a Style parameter, it is the suggested approach for customization. These styles allow you to access and modify specific properties within the composable's internal structure that might otherwise be inaccessible. \ No newline at end of file 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 new file mode 100644 index 0000000..8d62287 --- /dev/null +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md @@ -0,0 +1,257 @@ +> [!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: + +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. +2. Using Material Design + - **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible. + +## 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. + +With the Styles API, there's a new layer of abstraction that's a bridge between +subsystems and components: **Styles**. + +| Layer | Responsibility | Example | +|---|---|---| +| **Subsystem values** | Named values | `val Primary = Color(0xFF34A85E)` | +| **Atomic Styles** | Style that does exactly one property change | `val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic` | +| **Component Styles** | Component-specific configurations | A Button with Primary background and 16dp padding. `val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }` | +| **Components** | The functional UI element that consumes a Style. | `Button(style = buttonStyle) { ... }` | + +![Diagram showing Theming with Styles with the new layer introduction](https://developer.android.com/static/develop/ui/compose/styles/images/theming_styles_layer.png) **Figure 1.** An example of a component and how it accesses styles from a theme. + +### 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". + + +```kotlin +// Define single-purpose "atomic" styles +val paddingAtomic = Style { + contentPadding(16.dp) +} +val roundedCornerShapeAtomic = Style { + shape(RoundedCornerShape(8.dp)) +} +val primaryBackgroundAtomic = Style { + background(Color.Blue) +} +val largeSizeAtomic = Style { + size(100.dp, 40.dp) +} +val interactiveShadowAtomic = Style { + hovered { + animate { + dropShadow( + Shadow( + offset = DpOffset( + 0.dp, + 0.dp + ), + radius = 2.dp, + spread = 0.dp, + color = Color.Blue, + ) + ) + } + } +} +``` + +
+ +#### 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. + +**Traditional (non-atomic)**: + + +```kotlin +// One large monolithic style +val buttonStyle = Style { + contentPadding(16.dp) + 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. + +### 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*. + +***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. + +- **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. + +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{ + val buttonStyle: Style = Style { + shape(shapes.medium) + background(colors.brand) + contentColor(colors.textPrimary) + contentPaddingVertical(8.dp) + contentPaddingHorizontal(24.dp) + textStyle(typography.labelLarge) + disabled { + animate { + background(colors.brandSecondary) + } + } + } + val cardStyle: Style = Style { + shape(shapes.medium) + background(colors.uiBackground) + contentColor(colors.textPrimary) + } + } + ``` + +
+ +2. Provide `Styles` as part of your overall theme, and expose helper extension + functions on `StyleScope` to access the subsystems: + + + ```kotlin + @Immutable + class JetsnackTheme( + val colors: JetsnackColors = LightJetsnackColors, + val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), + val shapes: Shapes = Shapes() + ) { + companion object { + val colors: JetsnackColors + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.colors + + val typography: androidx.compose.material3.Typography + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.typography + + val shapes: Shapes + @Composable @ReadOnlyComposable + get() = LocalJetsnackTheme.current.shapes + + val styles: JetsnackStyles = JetsnackStyles + + val LocalJetsnackTheme: ProvidableCompositionLocal + get() = LocalJetsnackThemeInstance + } + } + + val StyleScope.colors: JetsnackColors + get() = LocalJetsnackTheme.currentValue.colors + + val StyleScope.typography: androidx.compose.material3.Typography + get() = LocalJetsnackTheme.currentValue.typography + + val StyleScope.shapes: Shapes + get() = LocalJetsnackTheme.currentValue.shapes + + internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } + + @Composable + fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors + val theme = JetsnackTheme(colors = colors) + + CompositionLocalProvider( + LocalJetsnackTheme provides theme, + ) { + MaterialTheme( + typography = LocalJetsnackTheme.current.typography, + shapes = LocalJetsnackTheme.current.shapes, + content = content, + ) + } + } + ``` + +
+ +3. Access `JetsnackStyles` within your composable: + + + ```kotlin + @Composable + fun CustomButton(modifier: Modifier, + style: Style = Style, + text: String) { + val interactionSource = remember { MutableInteractionSource() } + val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } + + // Apply style to top level container in combination with incoming style from parameter. + Box(modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + enabled = true, + role = Role.Button, + onClick = { + + }, + ) + .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { + 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 diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md new file mode 100644 index 0000000..8c2b77e --- /dev/null +++ b/play/engage-sdk-integration/SKILL.md @@ -0,0 +1,118 @@ +--- +name: engage-sdk-integration +description: Helps developers integrate, debug, and resolve Play Engage SDK implementation + issues. Use when adding Engage SDK support, generating publishing code, mapping + data classes to entities, or fixing SDK-related errors. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-14' + keywords: + - android + - engage + - engage sdk + - play engage library + - 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. + +## Workflow + +Follow these steps to assist the developer: + +1. **Identify Vertical and Cluster:** + + - Ask the developer which vertical their app belongs to based on **[references/schemas/](references/schemas)**. + - Check if the integration is for TV or Mobile. Read the TV-specific sections in [patterns.md](references/patterns.md) as well if the integration is for TV. + - Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. (e.g., `com.google.android.engage.food.service.AppEngageFoodClient`). + - **Note:** Initializing the client class requires a `Context` parameter (e.g., `AppEngageFoodClient(context)`). + - Always refer to [common.md](references/common.md) for common entities. + - Ask which cluster type they want to publish from the supported cluster types for that vertical. + - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. + - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. +2. **Generate Structured Boilerplate Code:** + + - Create a new directory for all Engage-related code. Name the directory to match the naming convention of the existing codebase. + - Generate the following classes using templates in [patterns.md](references/patterns.md): + - `Constants`: Holds constant values like attempt counts, publish types. + - `ItemToEntityConverter`: Converts app's local models to Engage's Entity models. + - `ClusterRequestFactory`: Constructs the publish requests. + - `EngageWorker`: Handles the actual publishing and publish errors using WorkManager. + - `EngagePublisher`: Orchestrates periodic and one-time jobs. + - `EngageBroadcastReceiver`: Listens for AppEngageService intents and starts a one-time publish job from `EngagePublisher`. **Important** : Implement both **static registration** and **dynamic registration** patterns, including the companion object `register` method inside the `EngageBroadcastReceiver` class. +3. **Suggest Entity Mapping:** + + - Ask the developer to provide their local model schema(e.g., a data class or a JSON snippet). + - If they haven't provided one, share entities from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory as a guide. + - Once the local model is identified, suggest a mapping to the corresponding Engage entity. + - Generate the conversion logic using the `ItemToEntityConverter` pattern in [patterns.md](references/patterns.md) and add it to the generated `{ENGAGE_CODE_DIR}/ItemToEntityConverter` +4. **Suggest Data Source:** + + - Ask the developer to provide the source of actual data you'll publish. + - Once the source of data is identified, use the source of data to fetch data in app's local model schema. + - Use `{ENGAGE_CODE_DIR}/ItemToEntityConverter` to convert this data to Engage entity. + - 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:** + + - Suggest updates to `build.gradle` and `AndroidManifest.xml`. + - For mobile apps, use [patterns.md](references/patterns.md). + - For TV apps, use the TV-specific sections in [patterns.md](references/patterns.md). + - 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: There's no separate import according to vertical except TV. For each vertical other than TV 'com.google.android.engage:engage-core:1.5.12' is enough. +6. **Debugging:** + + - 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: + \[ \] Verify that all the engage related files are created in + `{ENGAGE_CODE_DIR}/`: + - `Constants` + - `ItemToEntityConverter` + - `ClusterRequestFactory` + - `EngageWorker` + - `{cluster_type}Publisher` + - `EngageBroadcastReceiver` + \[ \] Verify that app's local model is converted to Engage entity by populating + the fields correctly in the model in `{ENGAGE_CODE_DIR}/ + ItemToEntityConverter`. + \[ \] Verify that `{ENGAGE_CODE_DIR}/EngageWorker` uses the data source + identified in Step 4. + \[ \] Verify that `EngageBroadcastReceiver.register(context)` is called within + the `Application` class or `MainActivity` to register the receiver + dynamically. + \[ \] Verify that `AndroidManifest.xml` contains the static `` + declaration for `EngageBroadcastReceiver` with the necessary intent actions. + + - **Important** : Explicitly instruct the developer to call `EngageBroadcastReceiver.register(context)` inside their custom `Application` class `onCreate()` (or their main activity `onCreate()`) to dynamically register the receiver. Stress that **both** static and dynamic registrations are required for the integration to function. + +## Reference Materials + +- **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:** + + - [Food Vertical](references/android/guide/playcore/engage/food.md) + - [Watch Vertical](references/android/guide/playcore/engage/watch.md) + - [Listen Vertical](references/android/guide/playcore/engage/listen.md) + - [Read Vertical](references/android/guide/playcore/engage/read.md) + - [Shopping Vertical](references/android/guide/playcore/engage/shopping.md) + - [Social Vertical](references/android/guide/playcore/engage/social.md) + - [Travel Vertical](references/android/guide/playcore/engage/travel.md) + - [Health \& Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) + - [Other Verticals](references/android/guide/playcore/engage/otherverticals.md) + - [TV Getting Started](references/android/guide/playcore/engage/tv/getting-started.md) + - [TV Recommendations](references/android/guide/playcore/engage/tv/recommendations.md) + - [TV Continue Watching](references/android/guide/playcore/engage/tv/continue-watching/index.md) + - [TV Entitlements](references/android/guide/playcore/engage/tv/entitlements.md) 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 new file mode 100644 index 0000000..976ad16 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md @@ -0,0 +1,434 @@ +## Publish FAQs + +#### 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. + +#### 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. + +#### 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. + +#### 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. + +## 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. + +#### 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). + +#### Background execution not allowed + +While registering the broadcast intent, you may come across the following error: + + Background execution not allowed: receiving Intent + { act=com.google.android.engage.action.PUBLISH_RECOMMENDATION .. } + +You need to register the broadcast receivers dynamically. + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION, + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null)); + ... + + } + +## Workflow FAQs + +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. + +#### Testing Deep links + +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". + +Developers can add their own tracking params and provide attribution to +calculate impact. + +## Engage for TV 2.0 FAQs + +### General Questions + +#### What is Continue Watching 2.0? + +Continue Watching 2.0 (Video Discovery API) 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 Continue Watching 2.0. + +This new system is designed to boost viewer engagement and retention by +providing a smooth, frictionless experience across the entire Google ecosystem. + +#### What are the benefits of using Continue Watching 2.0? + +Answer: Continue Watching 2.0 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: Continue Watching 2.0 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, Continue Watching 2.0 works across other Android media experiences, like Play Cubes 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! Continue Watching 2.0 is backward compatible, so your existing integration will still work. + +Important Note: All new Continue Watching integrations must use Continue +Watching 2.0. The older "Cross Device Play Next" system is being phased out. + +#### What surfaces support Continue Watching 2.0? + +1. Google TV +2. Android TV (on-device only but supports Engage SDK) +3. Google TV Android mobile app +4. Google TV iOS mobile app +5. Play Cubes +6. Google entertainment space +7. iOS devices (with REST API integration). + +#### Is Engage SDK for Continue Watching 2.0? + +Yes, the Engage SDK is for Continue Watching 2.0. It is required to integrate +with Continue Watching 2.0. + +#### Is Continue Watching 2.0 available for everyone? + +Continue Watching 2.0 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 Continue Watching 2.0 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 Continue Watching 2.0 +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. + +#### 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 advantages for Continue Watching, 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. + +- **Mobile app integration:** Continue Watching is available 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, Continue Watching prompts users to re-engage + with your app, boosting retention. + +- **Expansion to other platforms:** This integration extends Continue Watching + to other Google media platforms like Android, Play Cubes, 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. + +#### What happens if I try to publish more than 5 entities? + +EngageSDK 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. + +### 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. + +While we understand you might be confident in your implementation, the Continue +Watching 2.0 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. + +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. + +#### What are some common mistakes to watch out for during integration? + +The verification app is designed to catch potential issues with your Continue +Watching 2.0 integration. Here are some common mistakes that developers often +encounter: + +For all content types (movies, TV episodes, live streams, video clips): + +- Missing Links: Make sure you provide valid platform-specific URIs (links) for your content. These links tell the system where to find your content on each platform. +- Missing Titles: Don't forget to include titles for all your content. This helps users identify what they were watching. +- Image Aspect Ratios: Verify all images associated with your content have an aspect ratio close to 16:9. This makes sure your images display correctly on different screens. + +For TV Episodes: + +- Complete Episode Information: Make sure to include the show title, episode number, and season number. This helps organize episodes and allows users to navigate within a series. +- Accurate Playback Position: Double-check that the last playback position is less than or equal to the total duration of the episode. This makes sure users resume from the correct spot. + +For Movies: + +- Accurate Playback Position: Similar to TV episodes, verify the last playback position is accurate. + +For Live Streaming Videos: + +- Broadcaster Information: Include the broadcaster's name for live streams. + +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. + +### Account and Profile Questions + +#### My app uses anonymous user logins. Is AccountProfile still required for Continue Watching 2.0? + +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 Continue Watching 2.0 works in this scenario: + +- AccountProfile is technically required, but... you can still integrate Continue Watching 2.0 even if your app doesn't have a user account system. +- Limited to on-device use: The cross-device capabilities of Continue Watching 2.0 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 Continue Watching entries only appear on the specific device where the content was started. + +In summary: While you can integrate Continue Watching 2.0 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: + +- Consistent identification: accountId identifies the user, while profileId distinguishes between different profiles within that user's account (if applicable). Providing both makes sure that Continue Watching 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 Continue Watching but only use accountId when deleting content, the system may not be able to correctly identify and remove the intended items. + +#### Is profileId required for Continue Watching 2.0? + +- accountId is required. This identifies the user across devices. +- profileId is crucial for a good user experience. While technically optional, profileId is strongly recommended if your service supports multiple profiles (like many streaming services do). Why is it so important? Because without profileId, Continue Watching may show content from other profiles on the same account. This can lead to a confusing and frustrating experience for your users. +- In short: Providing profileId makes sure that Continue Watching accurately reflects each individual's viewing history. Unless your app doesn't support the concept of profile within an account, you should provide it. + +#### 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 accountId-profileId combination. Any Google device that has +that same Google Account logged in, would get the latest updated data from the +same associated accountId-profileId(s), in its ContinueWatching row. + +#### Is account linking required to implement Continue Watching 2.0? + +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. + +#### Can we set "sync across devices" to false? + +UserConsentToSyncAcrossDevices 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 only happens +on the 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. + +#### 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). + +### REST API Questions + +#### Is there documentation on the REST API? + +The ETA for REST API is March 2025, this is documented in Continue Watching 2.0 +Developer Docs. + +### Legacy Watch Next Questions + +#### Is the Video Discovery API replacing the Watch Next API? + +The Video Discovery API will be backward compatible on all Android TV devices +that support the Watch Next API. All developers should use Video Discovery API +(Continue Watching 2.0) to publish to the Continue Watching row. + +### 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. + +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. + +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 +3. Changing accounts on the GTV device should change the cards on the Continue Watching row. Only videos from the current account should show up. Sorted in recent order. 3p app profile CW will be intermixed. Note: CW for GoogleAccount2 will show 3P contents that GoogleAccount2 was engaged watching +4. Exit the app with BACK button \> Verify card is displayed in the "Continue watching" row +5. Hide the video in the "Continue Watching" row, it shouldn't show again Test if hidden content stays hidden beyond 24 hours and even after the app opens after 24 hrs. Confirm hiding one item does not hide multiple items. +6. Content availability in Continue watching with full metadata: Card image, App name, title, season episode # for TV contents +7. Check Progress displays in the progress bar +8. User watched the content until ending credits - content does not display in Continue Watching +9. Confirm no unwatched items show up in continue watching row +10. Confirm that the CW items are arranged chronologically based on when watch activity happened and not when the app was last opened or last day +11. Confirm that episode and season details on CW card match what was watched on episodic content +12. Confirm completed (items at credits or beyond) items don't show up in continue watching +13. Turn off the device halfway through watching the episode/movie/show. "Turn off the device halfway through watching the episode/movie/show. Verify on turning on the device and on other TV, CW displays the right card , at the right position and progress bar" +14. Turn off the device after completely watching episode 1, verify +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 + +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. + + > [!NOTE] + > **Note:** When the app is installed and logged in, the CW state would reflect the active 3P user logged into the 3P app. + + 1. Note: Continue Watching row shouldn't show 3P contents if the GoogleAccount is not yet associated with a 3P profile + +#### We are not seeing Continue Watching showing up on Google TV iOS app. What happened? + +You will need to send iOS deeplinks for Continue Watching to show up on iOS +devices. + +#### How often should I update Continue Watching information? Should I update the Continue Watching information frequently, like every 15 seconds? + +No, frequent updates are not recommended. Here's why: + +- Performance Impact: Continuously sending updates puts unnecessary strain on our servers, potentially slowing down the system for everyone. +- Unnecessary Data: While a user is actively watching, their playback position changes constantly. Sending updates every few seconds creates a lot of redundant data that isn't helpful for resuming playback. + +When to update Continue Watching information: + +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. +- When user removes an item from their continue watching row within app + +How to efficiently update: + +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 Continue Watching experience 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 new file mode 100644 index 0000000..fd2a79e --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md @@ -0,0 +1,1042 @@ +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. + +## Integration detail + +### Terminology + +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. + + - 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. + + ![](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. + + - 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. + + ![](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. + + - 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. + + ![](https://developer.android.com/static/images/guide/playcore/engage/food-reorder-cluster.png) **Figure:** Food Reorder cluster from a single partner. (\*UI for illustrative purposes only) + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Maximum entity limits in a cluster | +|---|---|---| +| Recommendation Cluster(s) | At most 7 | At most 50 (`ProductEntity`, `RecipeEntity`, or `StoreEntity`) | +| Featured Cluster | At most 1 | At most 20 (`ProductEntity`, `RecipeEntity`, or `StoreEntity`) | +| Food Shopping Cart Cluster | At most 1 | At most 3 `FoodShoppingCart` | +| Food Shopping List Cluster | At most 1 | At most 3 `FoodShoppingList` | +| Food Reorder Cluster | At most 1 | At most 1 `ReorderEntity` | + +### 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: + +1. `ProductEntity` +2. `StoreEntity` +3. `RecipeEntity` +4. `FoodShoppingCart` +5. `FoodShoppingList` +6. `FoodReorderCluster` + +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. + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-product-entity.png) **Figure :** Attributes of `ProductEntity` + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/food#image-specs) for guidance. | +| Action Uri | **Required** | The deep link to the page in the app displaying details about the product. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the product. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| Price - current | Conditionally required | The current price of the product. **Must be provided if strikethrough price is provided.** | Free text | +| Price - strikethrough | Optional | The original price of the entity, which is struck-through in the UI. | Free text | +| Callout | Optional | Callout to feature a promo, event, or update for the product, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout fine print | Optional | Fine print text for the callout. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| **Rating (Optional) - Note: All ratings are displayed using our standard star rating system.** |||| +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the product. **Note:** Provide this field if your app controls how the count is displayed to the users. Use a concise string. For example, if the count is 1,000,000, consider using an abbreviation like 1M so that the count isn't truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the product. **Note:** Provide this field if you don't handle the display abbreviation logic yourself. If both Count and Count Value are present, Count is displayed to users. | Long | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `StoreEntity` + +The `StoreEntity` object represents an individual store that developer partners +want to publish, such as a restaurant or a grocery store. + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-store-entity.png) **Figure :** Attributes of `StoreEntity` + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/food#image-specs) for guidance. | +| Action Uri | **Required** | The deep link to the page in the app displaying details about the store. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Location | Optional | The location of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout | Optional | Callout to feature a promo, event, or update for the store, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout fine print | Optional | Fine print text for the callout. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Description | Optional | A description of the store. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| Category | Optional | Category of a store, in the context of dining places, it can be cuisine like "french", "new american", "ramen", "fine dining". | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| **Note: All ratings is displayed using our standard star rating system.** |||| +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the store. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the store. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | + +#### `RecipeEntity` + +The `RecipeEntity` object represents a recipe item that developer partners want +to publish. + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-recipe-entity.png) **Figure :** Attributes of `RecipeEntity` + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/food#image-specs) for guidance. | +| Action Uri | **Required** | The deep link to the page in the app displaying details about the recipe. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the recipe. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Author | Optional | The author of the recipe. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Cook/Preparation time | Optional | The cooking time of the recipe. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout | Optional | Callout to feature a promo, event, or update for the recipe, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Category | Optional | The category of the recipe. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Description | Optional | A description of the recipe. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| **Note: All ratings are displayed using our standard star rating system.** |||| +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the recipe. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the recipe. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | + +#### `FoodShoppingCart` + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-shopping-cart-attributes.png) **Figure:** Food Shopping Cart cluster attributes. + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to the shopping cart in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Number of items | **Required** | The number of items (not just number of products) in the shopping cart. **For example: If there are 3 oranges and 1 apple in the cart, this number should be 4.** | Integer \>= 1 | +| Last user interaction timestamp | **Required** | Number of milliseconds elapsed from the epoch, identifying the last time when user interacted with the shopping cart. | Epoch timestamp in milliseconds | +| Title | Optional | The title of the cart (for example, *Your cart*). **If no title is provided by the developer, *Your cart* is the default.** | Free text **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Action Text | Optional | The call to action text of the button on the Shopping Cart (for example, *Your Shopping Bag*). **If no action text is provided by the developer, *View Cart* is the default.** This attribute is supported in version 1.1.0 onwards. | String | +| Cart images | Optional | Images of each product in the cart. **Up to 10 images can be provided in order of priority; the actual number of images displayed depends on the device form factor.** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/food#image-specs) for guidance. | +| Item labels | Optional | The list of labels for items on the shopping list. **The actual number of labels displayed depends on the device form factor.** | List of free text labels **Recommended text size: under 20 chars** (Text that is too long may show ellipses) | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `FoodShoppingList` + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-shopping-list.png) **Figure:** Food Shopping List cluster. + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to the shopping list in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Number of items | **Required** | The number of items in the shopping list. | Integer \>= 1 | +| Last user interaction timestamp | **Required** | Number of milliseconds elapsed from the epoch, identifying the last time when user interacted with the shopping list. | Epoch timestamp in milliseconds | +| Title | Optional | The title of the list (for example, *Your Grocery List*). **If no title is provided by the developer, *Shopping list* is the default.** | Free text **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Item labels | **Required** | The list of labels for items on the shopping list. **At least 1 label must be provided and up to 10 labels can be provided in order of priority; the actual number of labels displayed depends on the device form factor.** | List of free text labels **Recommended text size: under 20 chars** (Text that is too long may show ellipses) | + +#### `FoodReorderCluster` + +
+ +![](https://developer.android.com/static/images/guide/playcore/engage/food-reorder-cluster.png) **Figure:** Food Reorder cluster. + +
+ +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to reorder in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Action Text | Optional | The call to action text of the button on the Reorder (for example, *Order again*). **If no action text is provided by the developer, *Reorder* is the default.** This attribute is supported in version 1.1.0 onwards. | String | +| Number of items | **Required** | The number of items (not just number of products) in the previous order. **For example: If there were 3 small coffees and 1 croissant in the previous order, this number should be 4.** | Integer \>= 1 | +| Title | **Required** | The title of the reorder item. | Free text **Recommended text size: under 40 chars** (Text that is too long may show ellipses) | +| Item labels | Optional (If not provided, poster images should be provided) | The list of item labels for the previous order. **Up to 10 labels can be provided in order of priority; the actual number of labels displayed depends on the device form factor.** | List of free text **Recommended text size per label: under 20 chars** (Text that is too long may show ellipses) | +| Poster images | Optional (If not provided, item labels should be provided) | Images of the items in the previous order. **Up to 10 images can be provided in order of priority; the actual number of images displayed depends on the device form factor.** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/food#image-specs) for guidance. | + +#### Image specifications + +Required specifications for image assets are listed below: + +| Aspect ratio | Minimum pixels | Recommended pixels | +|---|---|---| +| Square (1x1) **Preferred** | 300x300 | 1200x1200 | +| Landscape (1.91x1) | 600x314 | 1200x628 | +| Portrait (4x5) | 480x600 | 960x1200 | + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +### 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). + +`AppEngageFoodClient` is responsible for publishing food clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishFoodShoppingCarts` +- `publishFoodShoppingLists` +- `publishReorderCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteFoodShoppingCartCluster` +- `deleteFoodShoppingListCluster` +- `deleteReorderCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +A `RecommendationCluster` object can have the following attributes: + +| Attribute | Requirement | Description | +|---|---|---| +| List of ProductEntity, StoreEntity, or RecipeEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | +| Title | **Required** | The title for the Recommendation Cluster (for example, *Big savings on Thanksgiving menu*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Subtitle | Optional | The subtitle for the Recommendation Cluster. | +| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Big savings on Thanksgiving menu") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Big savings on Thanksgiving menu") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a `FeaturedCluster` object. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + new FeaturedCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `publishFoodShoppingCarts` + +This API is used to publish a list of `FoodShoppingCart` objects. + +> [!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. + +### Kotlin + + client.publishFoodShoppingCarts( + PublishFoodShoppingCartsRequest.Builder() + .addFoodShoppingCart( + FoodShoppingCart.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFoodShoppingCarts( + new PublishFoodShoppingCartsRequest.Builder() + .addFoodShoppingCart( + new FoodShoppingCart.Builder() + ... + .build()) + .build()); + +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. + +#### `publishFoodShoppingLists` + +This API is used to publish a list of `FoodShoppingList` objects. + +> [!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. + +### Kotlin + + client.publishFoodShoppingLists( + PublishFoodShoppingListsRequest.Builder() + .addFoodShoppingList( + FoodShoppingListEntity.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFoodShoppingLists( + new PublishFoodShoppingListsRequest.Builder() + .addFoodShoppingList( + new FoodShoppingListEntity.Builder() + ... + .build()) + .build()); + +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. + +#### `publishReorderCluster` + +This API is used to publish a `FoodReorderCluster` object. + +> [!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. + +### Kotlin + + client.publishReorderCluster( + PublishReorderClusterRequest.Builder() + .setReorderCluster( + FoodReorderCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishReorderCluster( + new PublishReorderClusterRequest.Builder() + .setReorderCluster( + new FoodReorderCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFoodShoppingCartCluster` + +This API is used to delete the content of Food Shopping Cart Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFoodShoppingCartCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is availaile from version 1.1.0 onwards. + +#### `deleteFoodShoppingListCluster` + +This API is used to delete the content of Food Shopping List Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFoodShoppingListCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteReorderCluster` + +This API is used to delete the content of FoodReorderCluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteReorderCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION + // broadcast is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is + // received + // Trigger food shopping cart cluster publish when PUBLISH_FOOD_SHOPPING_CART broadcast + // is received + // Trigger food shopping list cluster publish when PUBLISH_FOOD_SHOPPING_LIST broadcast + // is received + // Trigger reorder cluster publish when PUBLISH_REORDER_CLUSTER broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register food Shopping Cart Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FOOD_SHOPPING_CART), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register food Shopping List Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FOOD_SHOPPING_LIST), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Reorder Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_REORDER_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger food shopping cart cluster publish when PUBLISH_FOOD_SHOPPING_CART broadcast is + // received + + // Trigger food shopping list cluster publish when PUBLISH_FOOD_SHOPPING_LIST broadcast is + // received + + // Trigger reorder cluster publish when PUBLISH_REORDER_CLUSTER broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register food Shopping Cart Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_FOOD_SHOPPING_CART), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register food Shopping List Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_FOOD_SHOPPING_LIST), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Reorder Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_REORDER_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.food.PUBLISH_FOOD_SHOPPING_CART` It is recommended to start a `publishFoodShoppingCarts` call when receiving this intent. +- `com.google.android.engage.action.food.PUBLISH_FOOD_SHOPPING_LIST` It is recommended to start a `publishFoodShoppingLists` call when receiving this intent. +- `com.google.android.engage.action.food.PUBLISH_REORDER_CLUSTER` It is recommended to start a `publishReorderCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google will perform a verification and review internally to make sure the integration works as expected. If changes are needed, Google will contact you with any necessary details. +- When testing is complete and no changes are needed, Google will contact you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , **Shopping Cart** , **Shopping List** , and **Reorder** clusters will be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..6bf12d7 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md @@ -0,0 +1,951 @@ +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. + +## Integration detail + +### Terminology + +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. + + - 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. + + - **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. + + ![](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. + + ![](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. + + ![](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. + + 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. + + ![](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 + + ![](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. + + - **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 + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 | At most 50 (`ArticleEntity`, `PersonEntity`, or `EventEntity`) | +| Continuation Cluster | At most 1 | At least 1 | At most 20 (`ArticleEntity`, or `EventReservationEntity`) | +| Featured Cluster | At most 1 | At least 1 | At most 20 (`GenericFeaturedEntity`) | + +### 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: + +1. `GenericFeaturedEntity` +2. `ArticleEntity` +3. `PersonEntity` +4. `EventEntity` +5. `EventReservationEntity` + +The charts below outline available attributes and requirements for each type. + +#### `GenericFeaturedEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, please ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Title | Optional | Title of the entity. | Free text **Recommended text size: 50 chars** | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, for example as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/healthandfitness#content-category) for guidance. | + +#### `ArticleEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text **Recommended text size: Max 50 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, please ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Source - Title | Optional | The name of the author, organization, or reporter | Free text **Recommended text size: Under 25 chars** | +| Source - Image | Optional | An image of the source like the author, the organization, reporter | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, for example as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Content Publish Time | Optional | This is the epoch timestamp in milliseconds on when the content was published / updated in the app. | Epoch timestamp in milliseconds | +| Last Engagement Time | Conditionally Required | The epoch timestamp in milliseconds when the user interacted with this entity last time. **Note:** This field is required if this entity is part of the continuation cluster. | Epoch timestamp in milliseconds | +| Progress Percentage | Conditionally Required | The percentage of the full content consumed by the user to date. **Note:** This field is required if this entity is part of the continuation cluster. | An int value between 0\~100 inclusive. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/healthandfitness#content-category) for guidance. | + +#### `PersonEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Profile - Name | **Required** | Profile name or id or handle, eg "John Doe", "@TeamPixel", etc. | String **Recommended text size: Max 50 chars** | +| Profile - Avatar | **Required** | Profile picture or avatar image of the user. **Note:**Must be Square 1:1 image. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Profile - Additional Text | Optional | Free text like the profile handle. | Free text **Recommended text size: Max 15 chars** | +| Profile - Additional Image | Optional | Small image like a verified badge. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Header image | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, please ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Popularity - Count | Optional | Indicate the number of followers or popularity value, for example - "3.7 M". **Note:** If both Count and Count Value are provided, Count will be used | String **Recommended text size: max 20 chars for count + label combined** | +| Popularity - Count Value | Optional | The number of followers or popularity value. **Note:** Provide Count Value if your app doesn't want to handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count will be used. | Long | +| Popularity - Label | Optional | Indicate what the popularity label is. For example - "Likes". | String **Recommended text size: Max 20 chars for count + label combined** | +| Popularity - Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, Emojis. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Rating - Max value | Required | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Required | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the entity. **Note:** Provide this field if your app controls how the count is displayed to the users. Use a concise string. For example, if the count is 1,000,000, consider using an abbreviation like 1M so that the count isn't truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the entity. **Note:** Provide this field if you don't handle the display abbreviation logic yourself. If both Count and Count Value are present, Count is displayed to users. | Long | +| Location - Country | Optional | The country where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - City | Optional | The city where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Optional | The address where the person is located or serving will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_HEALTH_AND_FITENESS (Example - Yoga/fitness trainer) - TYPE_HOME_AND_AUTO (Example - Plumber) - TYPE_SPORTS (Example - Player) - TYPE_DATING See the [Content Category section](https://developer.android.com/guide/playcore/engage/healthandfitness#content-category) for guidance. | + +#### `EventEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Start time | **Required** | The epoch timestamp when the event is expected to start. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, please ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Location - Country | Conditionally Required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally Required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally Required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| End time | Optional | The epoch timestamp when the event is expected to end. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/healthandfitness#content-category) for guidance. | + +#### `EventReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Start time | **Required** | The epoch timestamp when the event is expected to start. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Location - Country | Conditionally Required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally Required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally Required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, please ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| End time | Optional | The epoch timestamp when the event is expected to end. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, name of the event organizer/tour | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/healthandfitness#image-specs) for guidance. | +| Reservation ID | Optional | The reservation ID for the event reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the event. **Note:** Provide this field if your app wants to control how this is displayed to the users. Please provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the event. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/healthandfitness#content-category) for guidance. | + +#### Image specifications + +Required specifications for image assets are listed in this table: + +| Aspect ratio | Minimum pixels | Recommended pixels | +|---|---|---| +| Square (1x1) **Preferred** | 300x300 | 1200x1200 | +| 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. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +#### 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: + +- `TYPE_EDUCATION` +- `TYPE_SPORTS` +- `TYPE_MOVIES_AND_TV_SHOWS` +- `TYPE_BOOKS` +- `TYPE_AUDIOBOOKS` +- `TYPE_MUSIC` +- `TYPE_DIGITAL_GAMES` +- `TYPE_TRAVEL_AND_LOCAL` +- `TYPE_HOME_AND_AUTO` +- `TYPE_BUSINESS` +- `TYPE_NEWS` +- `TYPE_FOOD_AND_DRINK` +- `TYPE_SHOPPING` +- `TYPE_HEALTH_AND_FITENESS` +- `TYPE_MEDICAL` +- `TYPE_PARENTING` +- `TYPE_DATING` + +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: + + - 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. + +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). + +`AppEngagePublishClient` is responsible for publishing clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishContinuationCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteContinuationCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build() + ) + .build() + ) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClustersRequest.Builder() + .addFeaturedCluster( + new FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `publishContinuationCluster` + +This API is used to publish a `ContinuationCluster` object. + +> [!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. + +### Kotlin + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishContinuationCluster( + new PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + new ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinuationCluster` + +This API is used to delete the content of Continuation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinuationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(..) + .build()) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // do something + } else { + val exception = task.exception + if (exception is AppEngageException) { + @AppEngageErrorCode val errorCode = exception.errorCode + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + } + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continuation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continuation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.PUBLISH_CONTINUATION` It is recommended to start a `publishContinuationCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , and **Continuation** clusters may be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..8a4c22a --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md @@ -0,0 +1,947 @@ +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. + +## Integration detail + +### Terminology + +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. + + Your recommendations take the following structure: + - **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. + + ![](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. + + ![](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. + + ![](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). + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Maximum entity limits in a cluster | +|---|---|---| +| Recommendation Cluster(s) | At most 7 | At most 50 | +| Continuation Cluster | At most 1 | At most 20 | +| Featured Cluster | At most 1 | At most 20 | + +### 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: + +1. `MusicAlbumEntity` +2. `MusicArtistEntity` +3. `MusicTrackEntity` +4. `MusicVideoEntity` +5. `PlaylistEntity` +6. `PodcastSeriesEntity` +7. `PodcastEpisodeEntity` +8. `LiveRadioStationEntity` +9. `AudiobookEntity` + +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). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | The title of the music album. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Info page uri | **Required** | The deep link to the provider app for details about the music album. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Artists | **Required** | List of artists in the music album. | +| Playback uri | Optional | A deep link that starts playing the album in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Description | Optional | Must be within 200 characters if provided. | +| Songs count | Optional | The number of songs in the music album. | +| Genres | Optional | List of genres in the music album. | +| Album Format | Optional | ALBUM (includes LP and double LP) EP SINGLE Mixtape | +| Music labels | Optional | List of music labels associated with the album. | +| Downloaded on Device | Optional | Boolean indicating if the music album is downloaded on device. | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Release date | Optional | The release date of the album in epoch milliseconds. | +| Duration | Optional | The duration of the album in milliseconds. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | +| Progress percentage complete | Optional | Recommended for items in the Continuation Cluster. Integer between 0 and 100 | + +#### `MusicArtistEntity` + +The `MusicArtistEntity` object represents a music arist (for example, Adele). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Name of the music artist. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Info page uri | **Required** | The deep link to the provider app for details about the music artist. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Playback uri | Optional | The deep link which starts playing the artist's songs in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Description | Optional | Must be within 200 characters if provided. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | + +#### `MusicTrackEntity` + +The `MusicTrackEntity` object represents a music track (for example, *Yellow* by +Coldplay). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the music track. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Playback uri | **Required** | A deep link that starts playing the music track in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Artists | **Required** | List of artists for the music track. | +| Info page uri | Optional | A deep link to the provider app for details about the music track. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Description | Optional | Must be within 200 characters if provided. | +| Duration | Optional | The duration of the track in milliseconds. | +| Album | Optional | The name of the album to which the song belongs. | +| Downloaded on Device | Optional | Boolean indicating if the music track is downloaded on device. | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | +| Progress percentage complete | Optional | Recommended for items in the Continuation Cluster. Integer between 0 and 100 | + +#### `MusicVideoEntity` + +The `MusicVideoEntity` object represents a music video (for example, +*The Weeknd - Take My Breath (Official Music Video)*). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the music video. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Playback uri | **Required** | A deep link that starts playing the music video in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Info page uri | Optional | A deep link to the provider app for details about the music video. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Duration | Optional | The duration of the video in milliseconds. | +| View count | Optional | The number of views on the video in free text format. | +| Artists | Optional | List of artists of the music video. | +| Content rating | Optional | List of content ratings of the track. | +| Description | Optional | Must be within 200 characters if provided. | +| Downloaded on Device | Optional | Boolean indicating if the music video is downloaded on device. | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | +| Progress percentage complete | Optional | Recommended for items in the Continuation Cluster. Integer between 0 and 100 | + +#### `PlaylistEntity` + +The `PlaylistEntity` object represents a music playlist (for example, the US Top +10 Playlist). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the playlist. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Playback uri | **Required** | A deep link that starts playing the music playlist in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Info page uri | Optional | A deep link to the provider app for details about the music playlist. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Duration | Optional | The duration of the playlist in milliseconds. | +| Songs count | Optional | The number of songs in the music playlist. | +| Description | Optional | Must be within 200 characters if provided. | +| Downloaded on Device | Optional | Boolean indicating if the playlist is downloaded on device. | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | +| Progress percentage complete | Optional | Recommended for items in the Continuation Cluster. Integer between 0 and 100 | + +#### `PodcastSeriesEntity` + +The `PodcastSeriesEntity` object represents a podcast series (for example, *This +American Life*). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the podcast series. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Info page uri | **Required** | A deep link to the provider app for details about the podcast series. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Playback uri | Optional | A deep link that starts playing the podcast series in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Episode count | Optional | The number of episodes in the podcast series. | +| Production name | Optional | The name of the production of the podcast series. | +| Hosts | Optional | List of hosts of the podcast series. | +| Genres | Optional | List of genres of the podcast series. | +| Downloaded on device | Optional | Boolean indicating if the podcast is downloaded on the device. | +| Description | Optional | Must be within 200 characters if provided. | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | + +#### `PodcastEpisodeEntity` + +The `PodcastEpisodeEntity` object represents a podcast series (for example, +*Spark Bird, Episode 754: This American Life*). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the podcast episode. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Playback uri | **Required** | A deep link that starts playing the podcast episode in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Podcast series title | **Required** | The name of the podcast series to which the episode belongs. | +| Duration | **Required** | The duration of the podcast episode in milliseconds. | +| Publish Date | **Required** | Publish date of the podcast (in epoch milliseconds) | +| Info page uri | Optional | A deep link to the provider app for details about the podcast episode. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Production name | Optional | The name of the production of the podcast series. | +| Episode index | Optional | The index of the episode in the series (first index is 1). | +| Hosts | Optional | List of hosts of the podcast episode. | +| Genres | Optional | List of genres of the podcast episode. | +| Downloaded on device | Optional | Boolean indicating if the podcast episode is downloaded on the device. | +| Description | Optional | Must be within 200 characters if provided. | +| Video Podcast | Optional | Boolean indicating if the podcast episode has video content | +| Explicit | Optional | A boolean indicating if the content is explicit or not Items that contain explicit material or have a parental advisory warning should be set to TRUE. Explicit items appears with an "E" tag. | +| Listen Next Type | Optional | Recommended for Items in the Continuation Cluster TYPE_CONTINUE - Resume on a unfinished audio item. TYPE_NEXT - Continue on a new one of a series. TYPE_NEW - Newly released. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | +| Progress percentage complete | Optional | Recommended for items in the Continuation Cluster. Integer between 0 and 100 | + +#### `LiveRadioStationEntity` + +The `LiveRadioStationEntity` object represents a live radio station (for +example, 98.1 The Breeze). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | Title of the live radio station. | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Playback uri | **Required** | A deep link that starts playing the radio station in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Info page uri | Optional | A deep link to the provider app for details about the radio station. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Frequency | Optional | The frequency at which the radio station is broadcasted (for example, "98.1 FM"). | +| Show title | Optional | The current show that is playing on the radio station. | +| Hosts | Optional | List of hosts of the radio station. | +| Description | Optional | Must be within 200 characters if provided. | +| Last engagement time | Optional | Recommended for items in the Continuation Cluster. May be used for ranking. In epoch milliseconds | + +#### `AudiobookEntity` + +The `AudiobookEntity` object represents an audiobook (for example, the audiobook +of *Becoming* by Michelle Obama). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/listen#image-specs) for guidance. | +| Author | **Required** | At least one author name must be provided. | +| Action link uri | **Required** | The deep link to the provider app for the audiobook. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Narrator | Optional | At least one narrator's name must be provided. | +| Publish date | Optional | In epoch milliseconds if provided. | +| Description | Optional | Must be within 200 characters if provided. | +| Price | Optional | Free text | +| Duration | Optional | Must be a positive value if provided. | +| Genre | Optional | List of genres associated with the book. | +| Series name | Optional | Name of the series that the audiobook belongs to (for example, *Harry Potter*. | +| Series unit index | Optional | The index of the audiobook in the series, where 1 is the first audiobook in the series. For example, if *Harry Potter and the Prisoner of Azkaban* is the 3rd book in the series, this should be set to 3. | +| Continue book type | Optional | TYPE_CONTINUE - Resume on a unfinished book. TYPE_NEXT - Continue on a new one of a series. TYPE_NEW - Newly released. | +| Last Engagement Time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Progress Percentage Complete | Conditionally required | Must be provided when the item is in the Continuation cluster. \*Newly\* acquired audiobooks can be a part of the continue reading cluster. Value must be greater than 0 and less than 100. | +| **DisplayTimeWindow - Set a time window for a content to be shown on the surface** ||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | + +#### Image specifications + +Required specifications for image assets are listed below: + +| Aspect ratio | Requirement | Minimum pixels | Recommended pixels | +|---|---|---|---| +| Square (1x1) | **Required** | 300x300 | 1200x1200 | +| Landscape (1.91x1) | Optional | 600x314 | 1200x628 | +| Portrait (4x5) | Optional | 480x600 | 960x1200 | + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. + +#### Examples + + MusicAlbumEntity musicAlbumEntity = + new MusicAlbumEntity.Builder() + .setName(NAME) + .addPosterImage(new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(960) + .setImageWidthInPixel(408) + .build()) + .setPlayBackUri("https://play.google/album/play") + .setInfoPageUri("https://play.google/album/info") + .setDescription("A description of this album.") + .addArtist("Artist") + .addGenre("Genre") + .addMusicLabel("Label") + .addContentRating("Rating") + .setSongsCount(960) + .setReleaseDateEpochMillis(1633032895L) + .setDurationMillis(1633L) + .build(); + + AudiobookEntity audiobookEntity = + new AudiobookEntity.Builder() + .setName("Becoming") + .addPosterImage(new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(960) + .setImageWidthInPixel(408) + .build()) + .addAuthor("Michelle Obama") + .addNarrator("Michelle Obama") + .setActionLinkUri( + Uri.parse("https://play.google/audiobooks/1")) + .setDurationMillis(16335L) + .setPublishDateEpochMillis(1633032895L) + .setDescription("An intimate, powerful, and inspiring memoir") + .setPrice("$16.95") + .addGenre("biography") + .build(); + +### 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). + +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishContinuationCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteContinuationCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content + // publish calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Trending music") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Trending music") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + new FeaturedCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `publishContinuationCluster` + +This API is used to publish a `ContinuationCluster` object. + +> [!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. + +### Kotlin + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinuationCluster` + +This API is used to delete the content of Continuation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinuationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continuation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continuation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.PUBLISH_CONTINUATION` It is recommended to start a `publishContinuationCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google will perform a verification and review internally to make sure the integration works as expected. If changes are needed, Google will contact you with any necessary details. +- When testing is complete and no changes are needed, Google will contact you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , and **Continuation** clusters will be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..2e5ad1b --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md @@ -0,0 +1,1188 @@ +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. + +## Integration detail + +### Terminology + +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. + + - **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. + + ![](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. + + ![](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. + + ![](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. + + ![](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. + + ![](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. + + 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 + + ![](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. + + ![](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: + + - Sports events like reservation for a football match + - Gaming events like reservation for eSports + - Entertainment events like reservation for movies in a cinema, concert, theater, book signing + - Travel or point of interest reservations like guided tours, museum tickets + - Social / Seminar / Conferences reservations + - 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. + + ![](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. + + ![](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. + + ![](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. + + - **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 + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 | At most 50 (`ArticleEntity`, `EventEntity`, `LodgingEntity`, `StoreEntity`, `PointOfInterestEntity`, or `PersonEntity`) | +| Continuation Cluster | At most 1 | At least 1 | At most 20 (`ArticleEntity`, `EventReservationEntity`, `LodgingReservationEntity`, `TransportationReservationEntity`, or `VehicleRentalReservationEntity`) | +| Featured Cluster | At most 1 | At least 1 | At most 20 (`GenericFeaturedEntity`) | + +### 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: + +1. `GenericFeaturedEntity` +2. `ArticleEntity` +3. `EventEntity` +4. `LodgingEntity` +5. `StoreEntity` +6. `PointOfInterestEntity` +7. `PersonEntity` +8. `RestaurantReservationEntity` +9. `EventReservationEntity` +10. `LodgingReservationEntity` +11. `TransportationReservationEntity` +12. `VehicleRentalReservationEntity` + +The charts below outline available attributes and requirements for each type. + +#### `GenericFeaturedEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Title | Optional | Title of the entity. | Free text **Recommended text size: 50 chars** | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, for example, as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `ArticleEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text **Recommended text size: Max 50 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Source - Title | Optional | The name of the author, organization, or reporter | Free text **Recommended text size: Under 25 chars** | +| Source - Image | Optional | An image of the source like the author, the organization, reporter | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, for example as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Content Publish Time | Optional | This is the epoch timestamp in milliseconds on when the content was published / updated in the app. | Epoch timestamp in milliseconds | +| Last Engagement Time | Conditionally Required | The epoch timestamp in milliseconds when the user interacted with this entity last time. **Note:** This field is required if this entity is part of the continuation cluster. | Epoch timestamp in milliseconds | +| Progress Percentage | Conditionally Required | The percentage of the full content consumed by the user to date. **Note:** This field is required if this entity is part of the continuation cluster. | An int value between 0\~100 inclusive. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `EventEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Start time | **Required** | The epoch timestamp when the event is expected to start. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Location - Country | Conditionally Required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally Required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally Required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| End time | Optional | The epoch timestamp when the event is expected to end. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `LodgingEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Location - Country | **Required** | The country in which the lodging is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the lodging is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address of the lodging that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| AvailabilityTimeWindow - Start Time | Optional | The epoch timestamp in milliseconds when the lodging is expected to be open/available. | Epoch timestamp in milliseconds | +| AvailabilityTimeWindow - End Time | Optional | The epoch timestamp in milliseconds until which the lodging is expected to be open/available. | Epoch timestamp in milliseconds | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app controls how the count is displayed to the users. Use a concise string. For example, if the count is 1,000,000, consider using an abbreviation like 1M so that the count isn't truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you aren't handling the display abbreviation logic yourself. If both Count and Count Value are present, Count is displayed to users. | Long | +| Price - CurrentPrice | Conditionally required | The current price of the lodging. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the lodging, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | + +#### `StoreEntity` + +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 | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Location | Optional | The location of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout | Optional | Callout to feature a promo, event, or update for the store, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout fine print | Optional | Fine print text for the callout. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Description | Optional | A description of the store. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | + +#### `PointOfInterestEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Location - Country | **Required** | The country in which the point of interest is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the point of interest is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address of the point of interest that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the point of interest is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| AvailabilityTimeWindow - Start Time | Optional | The epoch timestamp in milliseconds when the point of interest is expected to be open/available. | Epoch timestamp in milliseconds | +| AvailabilityTimeWindow - End Time | Optional | The epoch timestamp in milliseconds until which the point of interest is expected to be open/available. | Epoch timestamp in milliseconds | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the point of interest. **Note:** Provide this field if your app controls how the count is displayed to the users. Use a concise string. For example, if the count is 1,000,000, consider using an abbreviation like 1M so that the count isn't truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the point of interest. **Note:** Provide this field if you aren't handling display abbreviation logic yourself. If both Count and Count Value are present, Count is displayed to users | Long | +| Price - CurrentPrice | Conditionally required | The current price of the tickets/entry pass for the point of interest. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the tickets/entry pass for the point of interest. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_TRAVEL_AND_LOCAL - TYPE_MOVIES_AND_TV_SHOWS (Example - theater) - TYPE_MEDICAL (Example - hospital) - TYPE_EDUCATION (Example - school) - TYPE_SPORTS (Example - stadium) See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `PersonEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Profile - Name | **Required** | Profile name or id or handle, eg "John Doe", "@TeamPixel", etc. | String **Recommended text size: Max 50 chars** | +| Profile - Avatar | **Required** | Profile picture or avatar image of the user. **Note:**Must be Square 1:1 image. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Profile - Additional Text | Optional | Free text like the profile handle. | Free text **Recommended text size: Max 15 chars** | +| Profile - Additional Image | Optional | Small image like a verified badge. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Header image | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Popularity - Count | Optional | Indicate the number of followers or popularity value, for example - "3.7 M". **Note:** If both Count and Count Value are provided, Count will be used | String **Recommended text size: max 20 chars for count + label combined** | +| Popularity - Count Value | Optional | The number of followers or popularity value. **Note:** Provide Count Value if your app doesn't want to handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count will be used. | Long | +| Popularity - Label | Optional | Indicate what the popularity label is. For example - "Likes". | String **Recommended text size: Max 20 chars for count + label combined** | +| Popularity - Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, Emojis. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Rating - Max value | Required | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Required | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the entity. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the entity. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Location - Country | Optional | The country where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - City | Optional | The city where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Optional | The address where the person is located or serving will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) where the person is located or serving. | Free text **Recommended text size: max \~20 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_HEALTH_AND_FITENESS (Example - Yoga/fitness trainer) - TYPE_HOME_AND_AUTO (Example - Plumber) - TYPE_SPORTS (Example - Player) - TYPE_DATING See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `RestaurantReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Reservation Start Time | **Required** | The epoch timestamp in milliseconds when the reservation is expected to start. | Epoch timestamp in milliseconds | +| Location - Country | **Required** | The country in which the restaurant is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the restaurant is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address of the prestaurant that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the restaurant is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Table Size | Optional | The number of people in the reservation group | Integer \> 0 | + +#### `EventReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Start time | **Required** | The epoch timestamp when the event is expected to start. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Location - Country | Conditionally Required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally Required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally Required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| End time | Optional | The epoch timestamp when the event is expected to end. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, name of the event organizer/tour | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Reservation ID | Optional | The reservation ID for the event reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the event. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the event. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/otherverticals#content-category) for guidance. | + +#### `LodgingReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "Your Stay from Dec 12th" **Recommended text size: Max 50 chars** | +| Check-in Time | **Required** | The epoch timestamp in milliseconds that represents the check in time for the reservation. | Epoch timestamp in milliseconds | +| Check-out Time | **Required** | The epoch timestamp in milliseconds that represents the check out time for the reservation. | Epoch timestamp in milliseconds | +| Location - Country | **Required** | The country in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address of the lodging that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Reservation ID | Optional | The reservation ID for the lodging reservation. | Free text | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Price - CurrentPrice | Conditionally required | The current price of the lodging. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the lodging, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | + +#### `TransportationReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "SFO to SAN" **Recommended text size: Max 50 chars** | +| Transportation Type | **Required** | The mode/type of transportation for the reservation. | Enum: FLIGHT, TRAIN, BUS, or FERRY | +| Departure Time | **Required** | The epoch timestamp in milliseconds that represents the departure time. | Epoch timestamp in milliseconds | +| Arrival Time | **Required** | The epoch timestamp in milliseconds that represents the arrival time. | Epoch timestamp in milliseconds | +| Departure Location - Country | Optional | The country of departure. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - City | Optional | The city of departure. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Display Address | Optional | The location of departure that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Street Address | Optional | The street address (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - State | Optional | The state or province (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Zip code | Optional | The zip code (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Neighborhood | Optional | The neighborhood (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Country | Optional | The country of arrival. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - City | Optional | The city of arrival. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Display Address | Optional | The location of arrival that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Street Address | Optional | The street address (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - State | Optional | The state or province (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Zip code | Optional | The zip code (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Neighborhood | Optional | The neighborhood (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, Airline name | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Reservation ID | Optional | The reservation ID for the transportation reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the reservation. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the reservation, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Transportation Number | Required | The flight number, bus number, train number, or ferry/cruise number. | Free text | +| Boarding Time | Required | The epoch timestamp that represents the boarding time for the reservation (if applicable) | Epoch timestamp in milliseconds | + +#### `VehicleRentalReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "Avis Union Square SF" **Recommended text size: Max 50 chars** | +| Pickup Time | **Required** | The epoch timestamp that represents the pick up time for the reservation. | Epoch timestamp in milliseconds | +| Return Time | Optional | The epoch timestamp that represents the check out time for the reservation. | Epoch timestamp in milliseconds | +| Pickup Address - Country | Optional | The country of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - City | Optional | The city of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Display Address | Optional | The pickup location that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Street Address | Optional | The street address (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - State | Optional | The state or province (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Zip code | Optional | The zip code (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Neighborhood | Optional | The neighborhood (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Country | Optional | The country of return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - City | Optional | The city of return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Display Address | Optional | The return location that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Street Address | Optional | The street address (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - State | Optional | The state or province (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Zip code | Optional | The zip code (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Neighborhood | Optional | The neighborhood (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, "Avis Car Rental" | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/otherverticals#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Confirmation ID | Optional | The confirmation ID for the vehicle rental reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the reservation. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the reservation, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | + +#### Image specifications + +Required specifications for image assets are listed in this table: + +| Aspect ratio | Minimum pixels | Recommended pixels | +|---|---|---| +| Square (1x1) **Preferred** | 300x300 | 1200x1200 | +| 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. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +#### 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: + +- `TYPE_EDUCATION` +- `TYPE_SPORTS` +- `TYPE_MOVIES_AND_TV_SHOWS` +- `TYPE_BOOKS` +- `TYPE_AUDIOBOOKS` +- `TYPE_MUSIC` +- `TYPE_DIGITAL_GAMES` +- `TYPE_TRAVEL_AND_LOCAL` +- `TYPE_HOME_AND_AUTO` +- `TYPE_BUSINESS` +- `TYPE_NEWS` +- `TYPE_FOOD_AND_DRINK` +- `TYPE_SHOPPING` +- `TYPE_HEALTH_AND_FITENESS` +- `TYPE_MEDICAL` +- `TYPE_PARENTING` +- `TYPE_DATING` + +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: + + - 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. + +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). + +`AppEngagePublishClient` is responsible for publishing clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishContinuationCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteContinuationCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build() + ) + .build() + ) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClustersRequest.Builder() + .addFeaturedCluster( + new FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `publishContinuationCluster` + +This API is used to publish a `ContinuationCluster` object. + +> [!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. + +### Kotlin + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishContinuationCluster( + new PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + new ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinuationCluster` + +This API is used to delete the content of Continuation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinuationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(..) + .build()) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // do something + } else { + val exception = task.exception + if (exception is AppEngageException) { + @AppEngageErrorCode val errorCode = exception.errorCode + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + } + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continuation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continuation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.PUBLISH_CONTINUATION` It is recommended to start a `publishContinuationCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , and **Continuation** clusters may be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..4edff8a --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md @@ -0,0 +1,807 @@ +> [!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)**. + +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**. + +- **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. + + ![](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. + + ![](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. + + ![](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. + + ![](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). + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Maximum entity limits in a cluster | +|---|---|---| +| Recommendation Cluster(s) | At most 7 | At most 50 | +| Continuation Cluster | At most 1 | At most 20 | +| Featured Cluster | At most 1 | At most 20 | + +### 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: + +1. `EbookEntity` +2. `AudiobookEntity` +3. `BookSeriesEntity` + +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). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/read#image-specs) for guidance. | +| Author | **Required** | At least one author name must be provided. | +| Action link uri | **Required** | The deep link to the provider app for the ebook. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Publish date | Optional | In epoch milliseconds if provided. | +| Description | Optional | Must be within 200 characters if provided. | +| Price | Optional | Free text | +| Page count | Optional | Must be a positive integer if provided. | +| Genre | Optional | List of genres associated with the book. | +| Series name | Optional | Name of the series that the ebook belongs to (for example, *Harry Potter*). | +| Series unit index | Optional | The index of the ebook in the series, where 1 is the first ebook in the series. For example, if *Harry Potter and the Prisoner of Azkaban* is the 3rd book in the series, this should be set to 3. | +| Continue book type | Optional | TYPE_CONTINUE - Resume on a unfinished book. TYPE_NEXT - Continue on a new one of a series. TYPE_NEW - Newly released. | +| Last Engagement Time | Conditionally required | Must be provided when the item is in the Continuation cluster. \*Newly\* acquired ebooks can be a part of the continue reading cluster. In epoch milliseconds. | +| Progress Percentage Complete | Conditionally required | Must be provided when the item is in the Continuation cluster. Value must be greater than 0 and less than 100. | +| **DisplayTimeWindow - Set a time window for a content to be shown on the surface** ||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | + +#### `AudiobookEntity` + +The `AudiobookEntity` object represents an audiobook (for example, the audiobook +of *Becoming* by Michelle Obama). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/read#image-specs) for guidance. | +| Author | **Required** | At least one author name must be provided. | +| Action link uri | **Required** | The deep link to the provider app for the audiobook. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Narrator | Optional | At least one narrator's name must be provided. | +| Publish date | Optional | In epoch milliseconds if provided. | +| Description | Optional | Must be within 200 characters if provided. | +| Price | Optional | Free text | +| Duration | Optional | Must be a positive value if provided. | +| Genre | Optional | List of genres associated with the book. | +| Series name | Optional | Name of the series that the audiobook belongs to (for example, *Harry Potter*. | +| Series unit index | Optional | The index of the audiobook in the series, where 1 is the first audiobook in the series. For example, if *Harry Potter and the Prisoner of Azkaban* is the 3rd book in the series, this should be set to 3. | +| Continue book type | Optional | TYPE_CONTINUE - Resume on a unfinished book. TYPE_NEXT - Continue on a new one of a series. TYPE_NEW - Newly released. | +| Last Engagement Time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Progress Percentage Complete | Conditionally required | Must be provided when the item is in the Continuation cluster. \*Newly\* acquired audiobooks can be a part of the continue reading cluster. Value must be greater than 0 and less than 100. | +| **DisplayTimeWindow - Set a time window for a content to be shown on the surface** ||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | + +#### `BookSeriesEntity` + +The `BookSeriesEntity` object represents a book series (for example, the *Harry +Potter* book series, which has 7 books). + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image must be provided. See [Image Specifications](https://developer.android.com/guide/playcore/engage/read#image-specs) for guidance. | +| Author | **Required** | At least one author name must be present. | +| Action link uri | **Required** | The deep link to the provider app for the audiobook or ebook. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Book count | **Required** | Number of books in the series. | +| Description | Optional | Must be within 200 characters if provided. | +| Genre | Optional | List of genres associated with the book. | +| Continue book type | Optional | TYPE_CONTINUE - Resume on a unfinished book. TYPE_NEXT - Continue on a new one of a series. TYPE_NEW - Newly released. | +| Last Engagement Time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Progress Percentage Complete | Conditionally required | Must be provided when the item is in the Continuation cluster. \*Newly\* acquired book series can be a part of the continue reading cluster. Value must be greater than 0 and less than 100. | +| **DisplayTimeWindow - Set a time window for a content to be shown on the surface** ||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. In epoch milliseconds. | + +#### Image specifications + +Required specifications for image assets are listed below: + +| Aspect ratio | Requirement | Minimum pixels | Recommended pixels | +|---|---|---|---| +| Square (1x1) | **Required** | 300x300 | 1200x1200 | +| Landscape (1.91x1) | Optional | 600x314 | 1200x628 | +| Portrait (4x5) | Optional | 480x600 | 960x1200 | + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. + +#### Example + + AudiobookEntity audiobookEntity = + new AudiobookEntity.Builder() + .setName("Becoming") + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(960) + .setImageWidthInPixel(408) + .build()) + .addAuthor("Michelle Obama") + .addNarrator("Michelle Obama") + .setActionLinkUri(Uri.parse("https://play.google/audiobooks/1")) + .setDurationMillis(16335L) + .setPublishDateEpochMillis(1633032895L) + .setDescription("An intimate, powerful, and inspiring memoir") + .setPrice("$16.95") + .addGenre("biography") + .build(); + +### 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). + +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishContinuationCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteContinuationCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content + // publish calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Reconnect with yourself") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Reconnect with yourself") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + new FeaturedCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `publishContinuationCluster` + +This API is used to publish a `ContinuationCluster` object. + +> [!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. + +### Kotlin + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(book_entity1) + .addEntity(book_entity2) + .build()) + .build()) + +### Java + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(book_entity1) + .addEntity(book_entity2) + .build()) + .build()) + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinuationCluster` + +This API is used to delete the content of Continuation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinuationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continuation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continuation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + +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. +- com.google.android.engage.action.PUBLISH_CONTINUATION`It is recommended to start a`publishContinuationCluster\` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google will perform a verification and review internally to make sure the integration works as expected. If changes are needed, Google will contact you with any necessary details. +- When testing is complete and no changes are needed, Google will contact you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , and **Continuation** clusters will be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..cc79de9 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md @@ -0,0 +1,1116 @@ +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. + +## Integration detail + +### Terminology + +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. + + Your recommendations take the following structure: + - **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 **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. + + - **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 **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: + + - 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. + + 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 + - **ShoppingOrderTrackingEntity**: An object representing a shopping order tracking preview for a single developer partner, to be displayed in the Shopping Order Tracking cluster. The ShoppingOrderTrackingEntity must show the status of the order and the order time. We strongly recommend populating the expected delivery time for ShoppingOrderTrackingEntity, as it's displayed to users when provided. + + > [!NOTE] + > **Note:** Provide multiple ShoppingOrderTrackingEntity objects if an order has been split into multiple shipments. + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +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 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 | +|---|---|---| +| Recommendation Cluster(s) | At most 7 | At most 50 `ShoppingEntity` | +| Featured Cluster | At most 1 | At most 20 `ShoppingEntity` | +| Shopping Cart Cluster | At most 1 | At most 3 `ShoppingCart` Multiple carts only expected for apps with separate carts per merchant. | +| Shopping List Cluster | At most 1 | At most 3 `ShoppingList` | +| Shopping Reorder Cluster | At most 1 | At most 1 `ReorderEntity` | +| Shopping Order Tracking Cluster | At most 3 | At most 3 `ShoppingOrderTrackingEntity` | + +### 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: + +1. `ShoppingEntity` +2. `ShoppingCart` +3. `ShoppingList` +4. `Reorder` +5. `ShoppingOrderTracking` + +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. + +##### `ShoppingEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/shopping#image-specs) for guidance. | +| Action Uri | **Required** | The deep link to the page in the app displaying details about the entity. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the entity. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| Price - current | Conditionally required | The current price of the entity. **Must be provided if strikethrough price is provided.** | Free text | +| Price - strikethrough | Optional | The original price of the entity, which is be struck-through in the UI. | Free text | +| Callout | Optional | Callout to feature a promo, event, or update for the entity, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout fine print | Optional | Fine print text for the callout. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| **Rating (Optional) - Note: All ratings are displayed using our standard star rating system.** |||| +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the entity. **Note:** Provide this field if your app controls how the count is displayed to the users. Use a concise string. For example, if the count is 1,000,000, consider using an abbreviation like 1M so that the count isn't truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the entity. **Note:** Provide this field if you don't handle the display abbreviation logic yourself. If both Count and Count Value are present, Count is displayed to users. | Long | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `ShoppingCart` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to the shopping cart in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Number of items | **Required** | The number of items (not just number of products) in the shopping cart. **For example: If there are 3 identical shirts and 1 hat in the cart, this number should be 4.** | Integer \>= 1 | +| Action Text | Optional | The call to action text of the button on the Shopping Cart (for example, *Your Shopping Bag*). **If no action text is provided by the developer, *View Cart* is the default.** This attribute is supported in version 1.1.0 onwards. | String | +| Title | Optional | The title of the cart (for example, *Your Shopping Bag*). **If no title is provided by the developer, *Your cart* is the default.** **If developer partner publishes a separate cart per merchant, please include *merchant name* in the title.** | Free text **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Cart images | Optional | Images of each product in the cart. **Up to 10 images can be provided in order of priority; the actual number of images displayed depends on the device form factor.** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/shopping#image-specs) for guidance. | +| Item labels | Optional | The list of labels for items on the shopping list. **The actual number of labels displayed depends on the device form factor.** | List of free text labels **Recommended text size: under 20 chars** (Text that is too long may show ellipses) | +| Last user interaction timestamp | Optional | Number of milliseconds elapsed from the epoch, identifying the last time when user interacted with the cart. **This will be passed as input by the developer partners publishing separate cart per merchant and maybe used for ranking.** | Epoch timestamp in milliseconds | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `ShoppingList` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to the shopping list in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Number of items | **Required** | The number of items in the shopping list. | Integer \>= 1 | +| Title | Optional | The title of the list (for example, *Your Grocery List*). **If no title is provided by the developer, *Shopping list* is the default.** | Free text **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Item labels | **Required** | The list of labels for items on the shopping list. **At least 1 label must be provided and up to 10 labels can be provided in order of priority; the actual number of labels displayed depends on the device form factor.** | List of free text labels **Recommended text size: under 20 chars** (Text that is too long may show ellipses) | +| Last user interaction timestamp | **Required** | Number of milliseconds elapsed from the epoch, identifying the last time when user interacted with the shopping list. | Epoch timestamp in milliseconds | + +#### `ShoppingReorderCluster` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | The deep link to reorder in the partner's app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Action Text | Optional | The call to action text of the button on the Reorder (for example, *Order again*). **If no action text is provided by the developer, *Reorder* is the default.** This attribute is supported in version 1.1.0 onwards. | String | +| Number of items | **Required** | The number of items (not just number of products) in the previous order. **For example: If there were 3 small coffees and 1 croissant in the previous order, this number should be 4.** | Integer \>= 1 | +| Title | **Required** | The title of the reorder item. | Free text **Recommended text size: under 40 chars** (Text that is too long may show ellipses) | +| Item labels | Optional (If not provided, poster images should be provided) | The list of item labels for the previous order. **Up to 10 labels can be provided in order of priority; the actual number of labels displayed depends on the device form factor.** | List of free text **Recommended text size per label: under 20 chars** (Text that is too long may show ellipses) | +| Poster images | Optional (If not provided, item labels should be provided) | Images of the items in the previous order. **Up to 10 images can be provided in order of priority; the actual number of images displayed depends on the device form factor.** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/shopping#image-specs) for guidance. | + +#### `ShoppingOrderTrackingCluster` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Title | **Required** | A short title of the package/items being tracked or the tracking number. | Free text **Recommended text size: 50 chars (Text that is too long will show ellipses)** | +| Order Type | **Required** | A short title of the package/items being tracked or the tracking number. | Enum: IN_STORE_PICKUP, SAME_DAY_DELIVERY, MULTI_DAY_DELIVERY | +| Status | **Required** | The current status of the order. **For example: "Running late", "In transit", "Delayed", "Shipped", "Delivered", "Out of stock", "Order ready"** | Free text **Recommended text size: 25 chars (Text that is too long will show ellipses)** | +| Order Time | **Required** | The epoch timestamp in milliseconds at which the order was placed. **Order time will be displayed if expected delivery time window is not present** | Epoch timestamp in milliseconds | +| Action Uri | **Required** | Deep link to the order tracking in the partner's app. | Uri | +| **OrderDeliveryTimeWindow (Optional) - Set a time window for the order that is being tracked from the time the order was placed to the time of expected/actual delivery.** |||| +| OrderDeliveryTimeWindow - Start Time | Optional | The epoch timestamp in milliseconds on/after which the order will be delivered or be ready for pickup. | Epoch timestamp in milliseconds | +| OrderDeliveryTimeWindow - End Time | Optional | The epoch timestamp in milliseconds on/before which the order will be delivered or be ready for pickup. | Epoch timestamp in milliseconds | +| Poster images | Optional | Image of one item/product that is part of the order. Recommended aspect ratio is 1:1 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/shopping#image-specs) for guidance. | +| Number of items | Optional | The number of items in the order. | Integer \>= 1 | +| Description | Optional | A single paragraph of text to describe the items in the order. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Order Value - CurrentPrice | Optional | The current value of the order. | Free text | +| Order number | Optional | The order number/ID that can be used to uniquely identify the order. | Free text **Recommended text size: max 25 chars** | +| Tracking number | Optional | The tracking number for the order/parcel delivery in case the order requires a delivery. | Free text **Recommended text size: max 25 chars** | + +#### Image specifications + +Required specifications for image assets are listed below: + +| Aspect ratio | Minimum pixels | Recommended pixels | +|---|---|---| +| Square (1x1) **Preferred for non featured clusters** | 300x300 | 1200x1200 | +| Landscape (1.91x1) **Preferred for featured clusters** | 600x314 | 1200x628 | +| Portrait (4x5) | 480x600 | 960x1200 | + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +### 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). + +`AppEngageShoppingClient` is responsible for publishing shopping clusters. + +Following APIs are exposed to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishShoppingCarts` +- `publishShoppingLists` +- `publishShoppingReorderCluster` +- `publishShoppingOrderTrackingCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteShoppingCartCluster` +- `deleteShoppingListCluster` +- `deleteShoppingReorderCluster` +- `deleteShoppingOrderTrackingCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content + // publish calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +A `RecommendationCluster` object can have the following attributes: + +| Attribute | Requirement | Description | +|---|---|---| +| List of ShoppingEntity | **Required** | A list of ShoppingEntity objects that make up the recommendations for this Recommendation Cluster. | +| Title | **Required** | The title for the Recommendation Cluster. **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Subtitle | Optional | The subtitle for the Recommendation Cluster. | +| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Black Friday Deals") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Black Friday Deals") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a `FeaturedCluster` object. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + new FeaturedCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `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. + +> [!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. + +### Kotlin + + client.publishShoppingCarts( + PublishShoppingCartClustersRequest.Builder() + .addShoppingCart( + ShoppingCart.Builder() + ... + .build()) + .build()) + +### Java + + client.publishShoppingCarts( + new PublishShoppingCartClustersRequest.Builder() + .addShoppingCart( + new ShoppingCart.Builder() + ... + .build()) + .build()) + +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. + +#### `publishShoppingLists` + +This API is used to publish a list of `ShoppingList` objects. + +> [!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. + +### Kotlin + + client.publishShoppingLists( + PublishShoppingListsRequest.Builder() + .addShoppingList( + ShoppingList.Builder() + ... + .build()) + .build()) + +### Java + + client.publishShoppingLists( + new PublishShoppingListsRequest.Builder() + .addShoppingList( + new ShoppingListEntity.Builder() + ... + .build()) + .build()); + +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. + +#### `publishShoppingReorderCluster` + +This API is used to publish a `ShoppingReorderCluster` object. + +> [!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. + +### Kotlin + + client.publishShoppingReorderCluster( + PublishShoppingReorderClusterRequest.Builder() + .setReorderCluster( + ShoppingReorderCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishShoppingReorderCluster( + new PublishShoppingReorderClusterRequest.Builder() + .setReorderCluster( + new ShoppingReorderCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `publishShoppingOrderTrackingCluster` + +This API is used to publish a `ShoppingOrderTrackingCluster` object. + +> [!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. + +### Kotlin + + client.publishShoppingOrderTrackingCluster( + PublishShoppingOrderTrackingClusterRequest.Builder() + .setShoppingOrderTrackingCluster( + ShoppingOrderTrackingCluster.Builder() + ... + .build()) + .build()) + +### Java + + client.publishShoppingOrderTrackingCluster( + new PublishShoppingOrderTrackingClusterRequest.Builder() + .setShoppingOrderTrackingCluster( + new ShoppingOrderTrackingCluster.Builder() + ... + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteShoppingCartCluster` + +This API is used to delete the content of Shopping Cart Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteShoppingCartCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteShoppingListCluster` + +This API is used to delete the content of Shopping List Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteShoppingListCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteShoppingReorderCluster` + +This API is used to delete the content of Shopping Reorder Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteShoppingReorderCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteShoppingOrderTrackingCluster` + +This API is used to delete the content of Shopping Order Tracking Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteShoppingOrderTrackingCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This API is available in versions 1.4.0 and higher. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(..) + .build()) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // do something + } else { + val exception = task.exception + if (exception is AppEngageException) { + @AppEngageErrorCode val errorCode = exception.errorCode + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + } + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION + // broadcast is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is + // received + // Trigger shopping cart cluster publish when PUBLISH_SHOPPING_CART broadcast + // is received + // Trigger shopping list cluster publish when PUBLISH_SHOPPING_LIST broadcast + // is received + // Trigger reorder cluster publish when PUBLISH_REORDER_CLUSTER broadcast is + // received + // Trigger shopping order tracking cluster publish when + // PUBLISH_SHOPPING_ORDER_TRACKING_CLUSTER broadcast is received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Shopping Cart Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_SHOPPING_CART), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Shopping List Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_SHOPPING_LIST), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Reorder Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_REORDER_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Shopping Order Tracking Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_SHOPPING_ORDER_TRACKING_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger shopping cart cluster publish when PUBLISH_SHOPPING_CART broadcast is + // received + + // Trigger shopping list cluster publish when PUBLISH_SHOPPING_LIST broadcast is + // received + + // Trigger reorder cluster publish when PUBLISH_REORDER_CLUSTER broadcast is + // received + + // Trigger reorder cluster publish when PUBLISH_SHOPPING_ORDER_TRACKING_CLUSTER + // broadcast is received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Shopping Cart Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_SHOPPING_CART), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Shopping List Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_SHOPPING_LIST), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Reorder Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_REORDER_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Shopping Order Tracking Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.shopping.service.Intents.ACTION_PUBLISH_SHOPPING_ORDER_TRACKING_CLUSTER), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.shopping.PUBLISH_SHOPPING_CART` It is recommended to start a `publishShoppingCarts` call when this intent is received. +- `com.google.android.engage.action.shopping.PUBLISH_SHOPPING_LIST` It is recommended to start a `publishShoppingLists` call when this intent is received. +- `com.google.android.engage.action.shopping.PUBLISH_REORDER_CLUSTER` It is recommended to start a `publishReorderCluster` call when this intent is received. +- `com.google.android.engage.action.shopping.PUBLISH_SHOPPING_ORDER_TRACKING_CLUSTER` It is recommended to start a `publishShoppingOrderTrackingCluster` call when this intent is received. + +## 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , **Shopping Cart** , **Shopping List** , **Reorder Cluster** and **Shopping Order Tracking Cluster** clusters may be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..def7f16 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md @@ -0,0 +1,626 @@ +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. + +## Integration detail + +The following section captures the integration detail. + +### Terminology + +***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. + +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. + +- Post + + - Image in portrait mode and Timestamp, or + - Image in portrait mode + text content and Timestamp +- Profile + + - Avatar, Name or Handle, Additional image +- Interactions + + - Count and label only, or + - Count and visual (icon) + +**SocialPostEntity** contains profile, post and interaction related metadata. + +- Profile + + - Avatar, Name or Handle, additional text, additional image +- Post + + - Text and Timestamp, or + - Rich media (image or rich URL) and Timestamp, or + - Text and rich media (image or rich URL) and Timestamp, or + - Video preview (thumbnail and duration) and Timestamp +- Interactions + + - Count \& label only, or + - Count \& visual (icon) + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 (`PortraitMediaEntity`, or `SocialPostEntity`) | At most 50 (`PortraitMediaEntity`, or `SocialPostEntity`) | + +### 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: + +1. `PortraitMediaEntity` +2. `SocialPostEntity` + +The charts below outline available attributes and requirements for each type. + +#### `PortraitMediaEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** for all surfaces other than Google TV | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** |||| +| Image(s) | Required | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| Is video content | Optional | Is the post a video? | boolean | +| Video duration | Optional | The duration of the video in milliseconds. | Long | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel" | String(recommended max 25 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge. for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Optional | Indicate the number of interactions, for example - "3.7 M.". **Note:** If both Count and Count Value are provided, Count will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Optional | Indicate the number of interactions with an optional label, for example - "3.7 M Likes.". **Note:** If both CountWithOptionalLabel and Count Value are provided, one of them will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Count Value | Optional | The number of interactions as a value. **Note:** Provide Count Value instead of Count if your app doesn't handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count is used. | Long | +| Label | Optional | Indicate what the interaction label is for. For example - "Likes". | String | +| Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `SocialPostEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback URIs | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** At least one of TextContent, Image or WebContent is required |||| +| Image(s) | Optional | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| **Video Content (Optional)** |||| +| Duration | Required | The duration of the video in milliseconds. | Long | +| Image | Required | Preview image of the video content. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Link Preview (Optional)** |||| +| Link Preview - Title | Required | Text to indicate the title of the web page content | String | +| Link Preview - Hostname | Required | Text to indicate the web page owner, eg "INSIDER" | String | +| Link Preview - Image | Optional | Hero image for the web content | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel." | String(recommended max 25 chars) | +| Additional Text | Optional | Could be used as profile id or handle or additional metadata For example "@John-Doe", "5M followers", "You might like", "Trending", "5 new posts" | String(recommended max 40 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge, for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Required | Indicate the number of interactions, for example - "3.7 M." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Required | Indicate the number of interactions with an optional label, for example - "3.7 M Likes." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Label | Optional If not provided, **Visual** must be provided. | Indicate what the interaction is for. For example - "Likes." | String (recommended max 20 chars for count + label combined) | +| Visual | Optional If not provided, **Label** must be provided. | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### Image specifications + +The images are required to be hosted on public CDNs so that Google can access +them. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +### 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) + +`AppEngageSocialClient` is responsible for publishing social clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content + // publish calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content + // publish calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list `RecommendationCluster` objects. + +A `RecommendationCluster` object can have the following attributes: + +| Attribute | Requirement | Description | +|---|---|---| +| List of SocialPostEntity, or PortraitMediaEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | +| Title | **Required** | The title for the Recommendation Cluster (for example, *Latest from your friends*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Subtitle | Optional | The subtitle for the Recommendation Cluster. | +| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | + +> [!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. + +> [!IMPORTANT] +> **Important:** For social apps, it's critical to update recommendations 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION + // broadcast is received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + +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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation**, clusters will be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..b563ae3 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md @@ -0,0 +1,1308 @@ +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. + +## Integration detail + +### Terminology + +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. + + - 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. + + - **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. + + ![](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. + + ![](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. + + ![](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. + + ![](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. + + ![](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. + + 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. + + ![](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: + + - Sports events like reservation for a football match + - Gaming events like reservation for eSports + - Entertainment events like reservation for movies in a cinema, concert, theater, book signing + - Travel or point of interest reservations like guided tours, museum tickets + - Social / Seminar / Conferences reservations + - 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. + + ![](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. + + ![](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. + + ![](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. + + - **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. + + - **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. + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 | At most 50 (`ArticleEntity`, `EventEntity`, `LodgingEntity`, `StoreEntity`, or `PointOfInterestEntity`) | +| Reservation Cluster | At most 1 | At least 1 | At most 20 (`RestaurantReservationEntity`, `EventReservationEntity`, `LodgingReservationEntity`, `TransportationReservationEntity`, or `VehicleRentalReservationEntity`) | +| Featured Cluster | At most 1 | At least 1 | At most 20 (`GenericFeaturedEntity`) | +| Continue Search Cluster | At most 1 | At least 1 | At most 3 (`PointOfInterestEntity`) | + +### 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: + +1. `GenericFeaturedEntity` +2. `ArticleEntity` +3. `EventEntity` +4. `LodgingEntity` +5. `StoreEntity` +6. `PointOfInterestEntity` +7. `RestaurantReservationEntity` +8. `EventReservationEntity` +9. `LodgingReservationEntity` +10. `TransportationReservationEntity` +11. `VehicleRentalReservationEntity` + +The charts below outline available attributes and requirements for each type. + +#### `GenericFeaturedEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Title | Optional | Title of the entity. | Free text **Recommended text size: 50 chars** | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, For example, as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/travel#content-category) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `ArticleEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text **Recommended text size: Max 50 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Source - Title | Optional | The name of the author, organization, or reporter | Free text **Recommended text size: Under 25 chars** | +| Source - Image | Optional | An image of the source like the author, the organization, reporter | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. Special UX treatment on top of image/video, for example, as badge overlay on the image - "Live update" - Article read duration | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Content Publish Time | Optional | This is the epoch timestamp in milliseconds on when the content was published / updated in the app. | Epoch timestamp in milliseconds | +| Last Engagement Time | Optional | The epoch timestamp in milliseconds when the user interacted with this entity last time. | Epoch timestamp in milliseconds | +| Progress Percentage | Optional | The percentage of the full content consumed by the user to date. | An int value between 0\~100 inclusive. | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Enums See the [Content Category section](https://developer.android.com/guide/playcore/engage/travel#content-category) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `EventEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Localized Start time - Timestamp | **Required** | The epoch timestamp when the event is expected to start. | Joda-Time Instant | +| Localized Start time - Timezone | **Required** | The timezone in which the event is expected to start. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Location - Country | Conditionally required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| End time | Optional | The epoch timestamp when the event is expected to end. **Note:**This will be represented in milliseconds. | Epoch timestamp in milliseconds | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/travel#content-category) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `LodgingEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Poster images | **Required** | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Location - Country | **Required** | The country in which the lodging is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the lodging is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address that will be displayed to the user. We recommend including the city name and possibly state or country for most use cases. Only include the street address or neighborhood if the user is near the location, the user is familiar with the location, or the city is included in the cluster title. If you include the street address, provide a succinct address, using abbreviations where possible (for example, "St" for "Street", "Ave" for "Avenue"). | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| AvailabilityTimeWindow - Localized Start Time - Timestamp | Optional | The epoch timestamp when the lodging is expected to be open/available. | Joda-Time Instant | +| AvailabilityTimeWindow - Localized Start Time - Timezone | Optional | The timezone in which the lodging is expected to be open/available. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| AvailabilityTimeWindow - Localized End Time - Timestamp | Optional | The epoch timestamp until which the lodging is expected to be open/available. | Joda-Time Instant | +| AvailabilityTimeWindow - Localized End Time - Timezone | Optional | The timezone in which the lodging is expected to be open/available. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Price - CurrentPrice | Conditionally required | The current price of the lodging. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the lodging, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `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. + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Poster images | **Required** | At least one image must be provided. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | Optional | The name of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Location | Optional | The location of the store. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout | Optional | Callout to feature a promo, event, or update for the store, if available. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Callout fine print | Optional | Fine print text for the callout. | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Description | Optional | A description of the store. | Free text **Recommended text size: under 90 chars** (Text that is too long may show ellipses) | +| Category | Optional | Category of a store, in the context of dining places, it can be cuisine like "french", "new american", "ramen", "fine dining". | Free text **Recommended text size: under 45 chars** (Text that is too long may show ellipses) | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `PointOfInterestEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Poster images | Conditionally required | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is required if the entity is part of recommendation cluster. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Last Engagement Time | Conditionally required | The epoch timestamp when the user last interacted with this entity. **Note:** This field is required if the entity is part of continue search cluster. | Joda-Time Instant | +| Location - Country | Conditionally required | The country in which the point of interest is happening. **Note:** This field is required if the entity is part of recommendation cluster. | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally required | The city in which the point of interest is happening. **Note:** This field is required if the entity is part of recommendation cluster. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally required | The address that will be displayed to the user. Provide a succinct address, using abbreviations where possible (for example, "St" for "Street", "Ave" for "Avenue"). This string may be truncated depending on the user's device and settings. Include the city name for clear identification. **Note:** This field is required if the entity is part of recommendation cluster. | Free text **Recommended text size: max \~35 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the point of interest is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the point of interest. | Free text **Recommended text size: max \~20 chars** | +| AvailabilityTimeWindow - Localized Start Time - Timestamp | Optional | The epoch timestamp when the point of interest is expected to be open/available. | Joda-Time Instant | +| AvailabilityTimeWindow - Localized Start Time - Timezone | Optional | The timezone in which the point of interest is expected to be open/available. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| AvailabilityTimeWindow - Localized End Time - Timestamp | Optional | The epoch timestamp until which the point of interest is expected to be open/available. | Joda-Time Instant | +| AvailabilityTimeWindow - Localized End Time - Timezone | Optional | The timezone in which the point of interest is expected to be open/available. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the point of interest. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the point of interest. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Price - CurrentPrice | Conditionally required | The current price of the tickets/entry pass for the point of interest. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the tickets/entry pass for the point of interest. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_TRAVEL_AND_LOCAL - TYPE_MOVIES_AND_TV_SHOWS (Example - theater) - TYPE_MEDICAL (Example - hospital) - TYPE_EDUCATION (Example - school) - TYPE_SPORTS (Example - stadium) See the [Content Category section](https://developer.android.com/guide/playcore/engage/travel#content-category) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `RestaurantReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Localized Reservation Start Time - Timestamp | **Required** | The epoch timestamp when the reservation is expected to start. | Joda-Time Instant | +| Localized Reservation Start Time - Timezone | **Required** | The timezone in which the reservation is expected to start. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Location - Country | **Required** | The country in which the restaurant is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the restaurant is happening. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address of the prestaurant that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the restaurant is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the restaurant. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Table Size | Optional | The number of people in the reservation group | Integer \> 0 | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `EventReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | String **Recommended text size: Max 50 chars** | +| Localized Start time - Timestamp | **Required** | The epoch timestamp when the event is expected to start. | Joda-Time Instant | +| Localized Start time - Timezone | **Required** | The timezone in which the event is expected to start. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Event mode | **Required** | A field to indicate whether the event will be virtual, in-person or both. | Enum: VIRTUAL, IN_PERSON, or HYBRID | +| Location - Country | Conditionally required | The country in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - City | Conditionally required | The city in which the event is happening. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | Conditionally required | The address or venue name where the event will take place that should be displayed to the user. **Note:** This is required for events which are IN_PERSON or HYBRID | Free text **Recommended text size: max \~20 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the location at which event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the location in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) in which the event is being hosted. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** Image is highly recommended. If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Localized End time - Timestamp | Optional | The epoch timestamp when the event is expected to end. | Joda-Time Instant | +| Localized End time - Timezone | Optional | The timezone in which the event is expected to end. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, name of the event organizer/tour | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Badges | Optional | Each badge is either free text (max 15 chars) or small image. | | +| Badge - Text | Optional | Title for the badge **Note:** Either text or image is required for the badge | Free text **Recommended text size: max 15 chars** | +| Badge - Image | Optional | Small image Special UX treatment, for example as badge overlay on the image/video thumbnail. **Note:** Either text or image is required for the badge | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Reservation ID | Optional | The reservation ID for the event reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the ticket/pass for the event. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the ticket/pass for the event. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the event. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the event. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Content Categories | Optional | Describe the category of the content in the entity. | List of Eligible Enums - TYPE_MOVIES_AND_TV_SHOWS (Example - Cinema) - TYPE_DIGITAL_GAMES (Example - eSports) - TYPE_MUSIC (Example - Concert) - TYPE_TRAVEL_AND_LOCAL (Example - Tour, festival) - TYPE_HEALTH_AND_FITENESS (Example - Yoga class) - TYPE_EDUCATION (Example - Class) - TYPE_SPORTS (Example - Football game) - TYPE_DATING (Example - meetup) See the [Content Category section](https://developer.android.com/guide/playcore/engage/travel#content-category) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `LodgingReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "Your Stay from Dec 12th" **Recommended text size: Max 50 chars** | +| Localized Check-in Time - Timestamp | **Required** | The epoch timestamp that represents the check in time for the reservation. | Joda-Time Instant | +| Localized Check-in Time - Timezone | **Required** | The timezone in which the check in time exists for the reservation. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Localized Check-out Time - Timestamp | **Required** | The epoch timestamp that represents the check out time for the reservation. | Joda-Time Instant | +| Localized Check-out Time - Timezone | **Required** | The timezone in which the check out time exists for the reservation. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Location - Country | **Required** | The country in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - City | **Required** | The city in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Display Address | **Required** | The address that will be displayed to the user. Provide a succinct address, using abbreviations where possible (for example, "St" for "Street", "Ave" for "Avenue"). This string may be truncated depending on the user's device and settings. Include the city name for clear identification. | Free text **Recommended text size: max \~35 chars** | +| Location - Street Address | Optional | The street address (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - State | Optional | The state or province (if applicable) in which the lodging is located. | Free text **Recommended text size: max \~20 chars** | +| Location - Zip code | Optional | The zip code (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Location - Neighborhood | Optional | The neighborhood (if applicable) of the lodging. | Free text **Recommended text size: max \~20 chars** | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 **Note:** If a badge is provided, ensure safe space of 24 dps at both the top and bottom of the image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Reservation ID | Optional | The reservation ID for the lodging reservation. | Free text | +| Rating - Max value | Optional | The maximum value of the rating scale. **Must be provided if current value of rating is also provided.** | Number \>= 0.0 | +| Rating - Current value | Optional | The current value of the rating scale. **Must be provided if maximum value of rating is also provided.** | Number \>= 0.0 | +| Rating - Count | Optional | The count of the ratings for the lodging. **Note:** Provide this field if your app wants to control how this is displayed to the users. Provide the concise string that can be displayed to the user. For example, if the count is 1,000,000, consider using abbreviations like 1M, so that it won't be truncated on smaller display sizes. | String | +| Rating - Count Value | Optional | The count of the ratings for the lodging. **Note:** Provide this field if you don't want to handle the display abbreviation logic yourself. If both Count and Count Value are present, we will use the Count to display to users | Long | +| Price - CurrentPrice | Conditionally required | The current price of the lodging. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the lodging, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `TransportationReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "SFO to SAN" **Recommended text size: Max 50 chars** | +| Transportation Type | **Required** | The mode/type of transportation for the reservation. | Enum: FLIGHT, TRAIN, BUS, or FERRY | +| Localized Departure Time - Timestamp | **Required** | The epoch timestamp that represents the departure time. | Joda-Time Instant | +| Localized Departure Time - Timezone | **Required** | The timezone of the departure time. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Localized Arrival Time - Timestamp | **Required** | The epoch timestamp that represents the arrival time. | Joda-Time Instant | +| Localized Arrival Time - Timezone | **Required** | The timezone of the arrival time. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Transportation Number | **Required** | The flight number, bus number, train number, or ferry/cruise number. | Free text | +| Localized Boarding Time - Timestamp | **Required** | The epoch timestamp that represents the boarding time for the reservation (if applicable) | Joda-Time Instant | +| Localized Boarding Time - Timezone | **Required** | The timezone of the boarding time for the reservation (if applicable) | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Departure Location - Country | Optional | The country of departure. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - City | Optional | The city of departure. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Display Address | Optional | The location of departure that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Street Address | Optional | The street address (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - State | Optional | The state or province (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Zip code | Optional | The zip code (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Departure Location - Neighborhood | Optional | The neighborhood (if applicable) of the departure location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Country | Optional | The country of arrival. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - City | Optional | The city of arrival. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Display Address | Optional | The location of arrival that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Street Address | Optional | The street address (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - State | Optional | The state or province (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Zip code | Optional | The zip code (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Arrival Location - Neighborhood | Optional | The neighborhood (if applicable) of the arrival location. | Free text **Recommended text size: max \~20 chars** | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, Airline name | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Reservation ID | Optional | The reservation ID for the transportation reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the reservation. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the reservation, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `VehicleRentalReservationEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action Uri | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | Uri | +| Title | **Required** | Title of the entity. | Free text. For example, "Avis Union Square SF" **Recommended text size: Max 50 chars** | +| Localized Pickup Time - Timestamp | **Required** | The epoch timestamp that represents the pick up time for the reservation. | Joda-Time Instant | +| Localized Pickup Time - Timezone | **Required** | The timezone of the pick up time for the reservation. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Localized Return Time - Timestamp | Optional | The epoch timestamp that represents the check out time for the reservation. | Joda-Time Instant | +| Localized Return Time - Timezone | Optional | The timezone of the check out time for the reservation. | Joda-Time DateTimeZone See [Timezone Specifications](https://developer.android.com/guide/playcore/engage/travel#tz-specs) for guidance. | +| Pickup Address - Country | Optional | The country of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - City | Optional | The city of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Display Address | Optional | The pickup location that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Street Address | Optional | The street address (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - State | Optional | The state or province (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Zip code | Optional | The zip code (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Pickup Address - Neighborhood | Optional | The neighborhood (if applicable) of the pickup location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Country | Optional | The country of return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - City | Optional | The city of return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Display Address | Optional | The return location that will be displayed to the user. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Street Address | Optional | The street address (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - State | Optional | The state or province (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Zip code | Optional | The zip code (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Return Address - Neighborhood | Optional | The neighborhood (if applicable) of the return location. | Free text **Recommended text size: max \~20 chars** | +| Service Provider - Name | Optional | The name of the service provider. **Note:**Either text or image is required for the service provider. | Free text. For example, "Avis Car Rental" | +| Service Provider - Image | Optional | The logo/image of the service provider. **Note:**Either text or image is required for the service provider. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Poster images | Optional | We will show only 1 image when multiple images are provided. Recommended aspect ratio is 16:9 | See [Image Specifications](https://developer.android.com/guide/playcore/engage/travel#image-specs) for guidance. | +| Description | Optional | A single paragraph of text to describe the entity. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size: 180 chars** | +| Subtitle list | Optional | Up to 3 subtitles, with each subtitle a single line of text. **Note:** Either description or subtitle list will be displayed to the user, not both. | Free text **Recommended text size for each subtitle: max 50 chars** | +| Confirmation ID | Optional | The confirmation ID for the vehicle rental reservation. | Free text | +| Price - CurrentPrice | Conditionally required | The current price of the reservation. **Must be provided if strikethrough price is provided.** | Free text | +| Price - StrikethroughPrice | Optional | The original price of the reservation, which is be struck-through in the UI. | Free text | +| Price Callout | Optional | Price callout to feature a promo, event, member discount, if available. | Free text **Recommended text size: under 45 chars (Text that is too long may show ellipses)** | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### Image specifications + +Required specifications for image assets are listed in this table: + +| Aspect ratio | Minimum pixels | Recommended pixels | +|---|---|---| +| Square (1x1) **Preferred** | 300x300 | 1200x1200 | +| 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. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +#### Timezone specifications + +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: + +- `TYPE_EDUCATION` +- `TYPE_SPORTS` +- `TYPE_MOVIES_AND_TV_SHOWS` +- `TYPE_BOOKS` +- `TYPE_AUDIOBOOKS` +- `TYPE_MUSIC` +- `TYPE_DIGITAL_GAMES` +- `TYPE_TRAVEL_AND_LOCAL` +- `TYPE_HOME_AND_AUTO` +- `TYPE_BUSINESS` +- `TYPE_NEWS` +- `TYPE_FOOD_AND_DRINK` +- `TYPE_SHOPPING` +- `TYPE_HEALTH_AND_FITENESS` +- `TYPE_MEDICAL` +- `TYPE_PARENTING` +- `TYPE_DATING` + +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: + + - 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. + +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). + +`AppEngageTravelClient` is responsible for publishing clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishReservationCluster` +- `publishContinueSearchCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteReservationCluster` +- `deleteContinueSearchCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build() + ) + .build() + ) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClustersRequest.Builder() + .addFeaturedCluster( + new FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `publishReservationCluster` + +This API is used to publish a `ReservationCluster` object. + +> [!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. + +### Kotlin + + client.publishReservationCluster( + PublishReservationClusterRequest.Builder() + .setReservationCluster( + ReservationCluster.Builder() + .addLodgingReservationEntity(lodgingReservationEntity) + .addVehicleRentalReservationEntity(vehicleRentalReservationEntity) + .addTransportationReservationEntity(transportationReservationEntity) + .addEventReservationEntity(eventReservationEntity) + .addRestaurantReservationEntity(restaurantReservationEntity) + .build()) + .build()) + +### Java + + client.publishReservationCluster( + new PublishReservationClusterRequest.Builder() + .setReservationCluster( + new ReservationCluster.Builder() + .addLodgingReservationEntity(lodgingReservationEntity) + .addVehicleRentalReservationEntity(vehicleRentalReservationEntity) + .addTransportationReservationEntity(transportationReservationEntity) + .addEventReservationEntity(eventReservationEntity) + .addRestaurantReservationEntity(restaurantReservationEntity) + .build()) + .build()); + +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. + +#### `publishContinueSearchCluster` + +This API is used to publish a list of `ContinueSearchCluster` objects. + +> [!IMPORTANT] +> **Important:** The publish APIs replace any existing content. **Don't** call delete and publish APIs subsequently to replace the content, as the publish APIs do that inherently. + +### Kotlin + + client.publishContinueSearchCluster( + PublishContinueSearchClusterRequest.Builder() + .setContinueSearchCluster( + ContinueSearchCluster.Builder() + .addPointOfInterestEntity(entity1) + .addPointOfInterestEntity(entity2) + .build()) + .build()) + +### Java + + client.publishContinueSearchCluster( + new PublishContinueSearchClusterRequest.Builder() + .setContinueSearchCluster( + new ContinueSearchCluster.Builder() + .addPointOfInterestEntity(entity1) + .addPointOfInterestEntity(entity2) + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteReservationCluster` + +This API is used to delete the content of Reservation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteReservationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinueSearchCluster` + +This API is used to delete the content of Continue Search Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinueSearchCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This API is available from version 1.5.6 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_RESERVATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + .addClusterType(ClusterType.TYPE_CONTINUE_SEARCH) + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_RESERVATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + .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. + +#### 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(..) + .build()) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // do something + } else { + val exception = task.exception + if (exception is AppEngageException) { + @AppEngageErrorCode val errorCode = exception.errorCode + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + } + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continue search cluster publish when PUBLISH_CONTINUE_SEARCH + // broadcast is received + // Trigger reservation cluster publish when PUBLISH_RESERVATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continue Search Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(com.google.android.engage.travel.service.Intents.ACTION_PUBLISH_CONTINUE_SEARCH), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Reservation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(com.google.android.engage.travel.service.Intents.ACTION_PUBLISH_RESERVATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continue search cluster publish when PUBLISH_CONTINUE_SEARCH + // broadcast is received + + // Trigger reservation cluster publish when PUBLISH_RESERVATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continue Search Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.travel.service.Intents.ACTION_PUBLISH_CONTINUE_SEARCH), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Reservation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.travel.service.Intents.ACTION_PUBLISH_RESERVATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.travel.PUBLISH_CONTINUE_SEARCH` It is recommended to start a `publishContinueSearchCluster` call when receiving this intent. +- `com.google.android.engage.action.travel.PUBLISH_RESERVATION` It is recommended to start a `publishReservationCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , **Reservation** , and **Continue Search** clusters may be published and visible to users. \ No newline at end of file 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 new file mode 100644 index 0000000..affe437 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md @@ -0,0 +1,18 @@ +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: + +- [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 the video +discovery APIs to send personalized user data to Google, build a common module +that you can import in both the mobile and TV apps, when to call the publish +and delete APIs, and 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 new file mode 100644 index 0000000..0984715 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md @@ -0,0 +1,353 @@ +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 integrating with the Video Discovery API](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. + +## Pre-work + +Complete the [Pre-work](https://developer.android.com/guide/playcore/engage/tv/getting-started#pre-work) instructions in the Getting Started guide. + +1. Publish subscription information on the following events: + 1. User logs in to your app. + 2. User switches between profiles (if profiles are supported). + 3. User purchases a new subscription. + 4. User upgrades an existing subscription. + 5. User subscription expires. + +## Integration + +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: + +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. + - `SUBSCRIPTION_TYPE_INACTIVE`: User has an account but no active subscription or trial. + + > [!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. + +3. `ProviderPackageName`: Specify the package name of the app that handles the + subscription. + +Example for the sample media provider feed. + + "actionAccessibilityRequirement": [ + { + "@type": "ActionAccessSpecification", + "category": "subscription", + "availabilityStarts": "2022-06-01T07:00:00Z", + "availabilityEnds": "2026-05-31T07:00:00Z", + "requiresSubscription": { + "@type": "MediaSubscription", + // Don't match this string, + // ID is only used to for reconciliation purpose + "@id": "https://www.example.com/971bfc78-d13a-4419", + // Don't match this, as name is only used for displaying purpose + "name": "Basic common name", + "commonTier": true + } + +The following example creates a `SubscriptionEntity` for a user: + + val subscription = SubscriptionEntity.Builder() + setSubscriptionType( + SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE + ) + .setProviderPackageName("com.google.android.example") + // Optional + // December 30, 2025 12:00:00AM in milliseconds since epoch + .setExpirationTimeMillis(1767052800000) + .build() + +### 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. + +This entitlement has the following fields: + +1. `Identifier`: Required identifier string for this entitlement. This must match one of the [entitlement identifiers](https://developers.google.com/actions/media/concepts/access-requirements#entitlement-identifier) (note that this isn't the ID field) provided in the media provider's feed published to Google TV. +2. `Name`: This is auxiliary information and is used for entitlement matching. While optional, providing a human readable entitlement name enhances understanding of user entitlements for both developers and support teams. For example: Sling Orange. +3. `ExpirationTimeMillis`: Optionally specify the expiration time in milliseconds for this entitlement, if it differs from the subscription expiration time. By default, the entitlement will expire with the expiry of subscription. + +For the following sample media provider feed snippet: + + "actionAccessibilityRequirement": [ + { + "@type": "ActionAccessSpecification", + "category": "subscription", + "availabilityStarts": "2022-06-01T07:00:00Z", + "availabilityEnds": "2026-05-31T07:00:00Z", + "requiresSubscription": { + "@type": "MediaSubscription", + // Don't match this string, + // ID is only used to for reconciliation purpose + "@id": "https://www.example.com/971bfc78-d13a-4419", + + // Don't match this, as name is only used for displaying purpose + "name": "Example entitlement name", + "commonTier": false, + // match this identifier in your API. This is the crucial + // entitlement identifier used for recommendation purpose. + "identifier": "example.com:entitlementString1" + } + +The following example creates a `SubscriptionEntity` for a subscribed user: + + // Subscription with entitlements. + // The entitlement expires at the same time as its subscription. + val subscription = SubscriptionEntity.Builder() + .setSubscriptionType( + SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE + ) + .setProviderPackageName("com.google.android.example") + // Optional + // December 30, 2025 12:00:00AM in milliseconds + .setExpirationTimeMillis(1767052800000) + .addEntitlement( + SubscriptionEntitlement.Builder() + // matches with the identifier in media provider feed + .setEntitlementId("example.com:entitlementString1") + .setDisplayName("entitlement name1") + .build() + ) + .build() + + // Subscription with entitlements + // The entitement has different expiration time from its subscription + val subscription = SubscriptionEntity.Builder() + .setSubscriptionType( + SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE + ) + .setProviderPackageName("com.google.android.example") + // Optional + // December 30, 2025 12:00:00AM in milliseconds + .setExpirationTimeMillis(1767052800000) + .addEntitlement( + SubscriptionEntitlement.Builder() + .setEntitlementId("example.com:entitlementString1") + .setDisplayName("entitlement name1") + // You may set the expiration time for entitlement + // December 15, 2025 10:00:00 AM in milliseconds + .setExpirationTimeMillis(1765792800000) + .build()) + .build() + +### 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. + +Following code sample demonstrate how to create user subscription. + + // Subscription for linked service package + val subscription = SubscriptionEntity.Builder() + .setSubscriptionType( + SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE + ) + .setProviderPackageName("com.google.android.example") + // Optional + // December 30, 2025 12:00:00AM in milliseconds since epoch + .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. + + // Subscription for linked service package + val linkedSubscription = Subscription.Builder() + .setSubscriptionType( + SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE + ) + .setProviderPackageName("linked service package name") + // Optional + // December 30, 2025 12:00:00AM in milliseconds since epoch + .setExpirationTimeMillis(1767052800000) + .addBundledSubscription( + BundledSubscription.Builder() + .setBundledSubscriptionProviderPackageName( + "bundled-subscription-package-name" + ) + .setSubscriptionType(SubscriptionType.SUBSCRIPTION_TYPE_ACTIVE) + .setExpirationTimeMillis(111) + .addEntitlement( + SubscriptionEntitlement.Builder() + .setExpirationTimeMillis(111) + .setDisplayName("Silver subscription") + .setEntitlementId("subscription.tier.platinum") + .build() + ) + .build() + ) + .build() + +Optionally, add entitlements to a linked service subscription too. + +### Provide subscription set + +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. + +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() + .setAccountProfile(accountProfile) + .setSubscription(subscription) + .build() + ) + +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. + +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. + +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 Video discovery 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 video discovery + 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: + + // If the user logs out from your media app, you must make the following call + // to remove subscription and other video discovery data from the current + // google TV device. + client.deleteClusters( + new DeleteClustersRequest.Builder() + .setAccountProfile(accountProfile) + .setReason(DeleteReason.DELETE_REASON_USER_LOG_OUT) + .build() + ) + + 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 video discovery data from all google + // TV devices. + client.deleteClusters( + new DeleteClustersRequest.Builder() + .setAccountProfile(accountProfile) + .setReason(DeleteReason.DELETE_REASON_LOSS_OF_CONSENT) + .build() + ) + + 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 video discovery data. + client.deleteClusters( + new DeleteClustersRequest.Builder() + .setAccountProfile(accountProfile) + .setReason(DeleteReason.DELETE_REASON_ACCOUNT_PROFILE_DELETION) + .build() + ) + +### Testing + +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. + +2. Publish when: + + - User logs in for the first time. + - User changes profile (if profiles are supported). + - 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. + +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. + +5. Go to app and perform each of the following actions: + + - Sign in. + - Switch between profiles (if supported). + - Purchase a new subscription. + - Upgrade an existing subscription. + - Expire the subscription. + +#### Verify integration + +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. + + ![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 + + ![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. + + ![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 new file mode 100644 index 0000000..0d7ffdc --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md @@ -0,0 +1,167 @@ +
+ +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) +- [Publish recommendations](https://developer.android.com/guide/playcore/engage/tv/recommendations) + +## Pre-work + +Before you begin, complete the following steps: + +1. [Express interest in developing the Video Discovery API](http://g.co/tv/vda) to enroll in + the program, if eligible. + +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. + + ### Mobile + + dependencies { + implementation 'com.google.android.engage:engage-core:1.5.12' + } + + ### TV + + dependencies { + implementation 'com.google.android.engage:engage-tv:1.0.6' + } + +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. + +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 in + the `AndroidManifest.xml` file. + + + + > [!IMPORTANT] + > **Important:** The Engage service environment declaration is only needed for your release build. For your local testing with the verification app, don't include this element. + +## Common integration steps + +### Initialize the client + +Use `AppEngagePublishClient` to interact with the service. Always check if the +service is available before publishing. + + val client = AppEngagePublishClient(context) + + client.isServiceAvailable().addOnCompleteListener { task -> + if (task.isSuccessful && task.result) { + // Service is available, proceed with publishing + } else { + // Service is not available or call failed + } + } + +### Create an account profile + +`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") + .setProfileId("your_users_profile_id") // Optional + .setLocale(Locale.US.toLanguageTag()) // Optional, e.g., "en-US" + .build() + +## Testing + +To test your integration, download the verification app: + +[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. + +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`. +3. Enter the package name of your app. It automatically shows the published data. +4. Run your app and perform publishing actions, such as logging in or watching a video. +5. Verify that the data shows up in the verification app. + +## Download + +Before downloading, you must agree to the following terms and conditions. + +## Terms and Conditions + +This is the Android Software Development Kit License Agreement + +### 1. Introduction + +1.1 The Android Software Development Kit (referred to in the License Agreement as the "SDK" and specifically including the Android system files, packaged APIs, and Google APIs add-ons) is licensed to you subject to the terms of the License Agreement. The License Agreement forms a legally binding contract between you and Google in relation to your use of the SDK. 1.2 "Android" means the Android software stack for devices, as made available under the Android Open Source Project, which is located at the following URL: https://source.android.com , as updated from time to time. 1.3 A "compatible implementation" means any Android device that (i) complies with the Android Compatibility Definition document, which can be found at the Android compatibility website (https://source.android.com/compatibility) and which may be updated from time to time; and (ii) successfully passes the Android Compatibility Test Suite (CTS). 1.4 "Google" means Google LLC, organized under the laws of the State of Delaware, USA, and operating under the laws of the USA with principal place of business at 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA. + +### 2. Accepting this License Agreement + +2.1 In order to use the SDK, you must first agree to the License Agreement. You may not use the SDK if you do not accept the License Agreement. 2.2 By clicking to accept and/or using this SDK, you hereby agree to the terms of the License Agreement. 2.3 You may not use the SDK and may not accept the License Agreement if you are a person barred from receiving the SDK under the laws of the United States or other countries, including the country in which you are resident or from which you use the SDK. 2.4 If you are agreeing to be bound by the License Agreement on behalf of your employer or other entity, you represent and warrant that you have full legal authority to bind your employer or such entity to the License Agreement. If you do not have the requisite authority, you may not accept the License Agreement or use the SDK on behalf of your employer or other entity. + +### 3. SDK License from Google + +3.1 Subject to the terms of the License Agreement, Google grants you a limited, worldwide, royalty-free, non-assignable, non-exclusive, and non-sublicensable license to use the SDK solely to develop applications for compatible implementations of Android. 3.2 You may not use this SDK to develop applications for other platforms (including non-compatible implementations of Android) or to develop another SDK. You are of course free to develop applications for other platforms, including non-compatible implementations of Android, provided that this SDK is not used for that purpose. 3.3 You agree that Google or third parties own all legal right, title and interest in and to the SDK, including any Intellectual Property Rights that subsist in the SDK. "Intellectual Property Rights" means any and all rights under patent law, copyright law, trade secret law, trademark law, and any and all other proprietary rights. Google reserves all rights not expressly granted to you. 3.4 You may not use the SDK for any purpose not expressly permitted by the License Agreement. Except to the extent required by applicable third party licenses, you may not copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse engineer, disassemble, or create derivative works of the SDK or any part of the SDK. 3.5 Use, reproduction and distribution of components of the SDK licensed under an open source software license are governed solely by the terms of that open source software license and not the License Agreement. 3.6 You agree that the form and nature of the SDK that Google provides may change without prior notice to you and that future versions of the SDK may be incompatible with applications developed on previous versions of the SDK. You agree that Google may stop (permanently or temporarily) providing the SDK (or any features within the SDK) to you or to users generally at Google's sole discretion, without prior notice to you. 3.7 Nothing in the License Agreement gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, or other distinctive brand features. 3.8 You agree that you will not remove, obscure, or alter any proprietary rights notices (including copyright and trademark notices) that may be affixed to or contained within the SDK. + +### 4. Use of the SDK by You + +4.1 Google agrees that it obtains no right, title or interest from you (or your licensors) under the License Agreement in or to any software applications that you develop using the SDK, including any intellectual property rights that subsist in those applications. 4.2 You agree to use the SDK and write applications only for purposes that are permitted by (a) the License Agreement and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries). 4.3 You agree that if you use the SDK to develop applications for general public users, you will protect the privacy and legal rights of those users. If the users provide you with user names, passwords, or other login information or personal information, you must make the users aware that the information will be available to your application, and you must provide legally adequate privacy notice and protection for those users. If your application stores personal or sensitive information provided by users, it must do so securely. If the user provides your application with Google Account information, your application may only use that information to access the user's Google Account when, and for the limited purposes for which, the user has given you permission to do so. 4.4 You agree that you will not engage in any activity with the SDK, including the development or distribution of an application, that interferes with, disrupts, damages, or accesses in an unauthorized manner the servers, networks, or other properties or services of any third party including, but not limited to, Google or any mobile communications carrier. 4.5 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any data, content, or resources that you create, transmit or display through Android and/or applications for Android, and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. 4.6 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any breach of your obligations under the License Agreement, any applicable third party contract or Terms of Service, or any applicable law or regulation, and for the consequences (including any loss or damage which Google or any third party may suffer) of any such breach. + +### 5. Your Developer Credentials + +5.1 You agree that you are responsible for maintaining the confidentiality of any developer credentials that may be issued to you by Google or which you may choose yourself and that you will be solely responsible for all applications that are developed under your developer credentials. + +### 6. Privacy and Information + +6.1 In order to continually innovate and improve the SDK, Google may collect certain usage statistics from the software including but not limited to a unique identifier, associated IP address, version number of the software, and information on which tools and/or services in the SDK are being used and how they are being used. Before any of this information is collected, the SDK will notify you and seek your consent. If you withhold consent, the information will not be collected. 6.2 The data collected is examined in the aggregate to improve the SDK and is maintained in accordance with Google's Privacy Policy, which is located at the following URL: https://policies.google.com/privacy 6.3 Anonymized and aggregated sets of the data may be shared with Google partners to improve the SDK. + +### 7. Third Party Applications + +7.1 If you use the SDK to run applications developed by a third party or that access data, content or resources provided by a third party, you agree that Google is not responsible for those applications, data, content, or resources. You understand that all data, content or resources which you may access through such third party applications are the sole responsibility of the person from which they originated and that Google is not liable for any loss or damage that you may experience as a result of the use or access of any of those third party applications, data, content, or resources. 7.2 You should be aware the data, content, and resources presented to you through such a third party application may be protected by intellectual property rights which are owned by the providers (or by other persons or companies on their behalf). You may not modify, rent, lease, loan, sell, distribute or create derivative works based on these data, content, or resources (either in whole or in part) unless you have been specifically given permission to do so by the relevant owners. 7.3 You acknowledge that your use of such third party applications, data, content, or resources may be subject to separate terms between you and the relevant third party. In that case, the License Agreement does not affect your legal relationship with these third parties. + +### 8. Using Android APIs + +8.1 Google Data APIs 8.1.1 If you use any API to retrieve data from Google, you acknowledge that the data may be protected by intellectual property rights which are owned by Google or those parties that provide the data (or by other persons or companies on their behalf). Your use of any such API may be subject to additional Terms of Service. You may not modify, rent, lease, loan, sell, distribute or create derivative works based on this data (either in whole or in part) unless allowed by the relevant Terms of Service. 8.1.2 If you use any API to retrieve a user's data from Google, you acknowledge and agree that you will retrieve data only with the user's explicit consent and only when, and for the limited purposes for which, the user has given you permission to do so. If you use the Android Recognition Service API, documented at the following URL: https://developer.android.com/reference/android/speech/RecognitionService, as updated from time to time, you acknowledge that the use of the API is subject to the Data Processing Addendum for Products where Google is a Data Processor, which is located at the following URL: https://privacy.google.com/businesses/gdprprocessorterms/, as updated from time to time. By clicking to accept, you hereby agree to the terms of the Data Processing Addendum for Products where Google is a Data Processor. + +### 9. Terminating this License Agreement + +9.1 The License Agreement will continue to apply until terminated by either you or Google as set out below. 9.2 If you want to terminate the License Agreement, you may do so by ceasing your use of the SDK and any relevant developer credentials. 9.3 Google may at any time, terminate the License Agreement with you if: (A) you have breached any provision of the License Agreement; or (B) Google is required to do so by law; or (C) the partner with whom Google offered certain parts of SDK (such as APIs) to you has terminated its relationship with Google or ceased to offer certain parts of the SDK to you; or (D) Google decides to no longer provide the SDK or certain parts of the SDK to users in the country in which you are resident or from which you use the service, or the provision of the SDK or certain SDK services to you by Google is, in Google's sole discretion, no longer commercially viable. 9.4 When the License Agreement comes to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the License Agreement has been in force) or which are expressed to continue indefinitely, will be unaffected by this cessation, and the provisions of paragraph 14.7 will continue to apply to such rights, obligations and liabilities indefinitely. + +### 10. DISCLAIMER OF WARRANTIES + +10.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SDK IS AT YOUR SOLE RISK AND THAT THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND FROM GOOGLE. 10.2 YOUR USE OF THE SDK AND ANY MATERIAL DOWNLOADED OR OTHERWISE OBTAINED THROUGH THE USE OF THE SDK IS AT YOUR OWN DISCRETION AND RISK AND YOU ARE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR COMPUTER SYSTEM OR OTHER DEVICE OR LOSS OF DATA THAT RESULTS FROM SUCH USE. 10.3 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + +### 11. LIMITATION OF LIABILITY + +11.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS WILL NOT BE LIABLE TO YOU UNDER ANY THEORY OF LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES THAT MAY BE INCURRED BY YOU, INCLUDING ANY LOSS OF DATA, WHETHER OR NOT GOOGLE OR ITS REPRESENTATIVES HAVE BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING. + +### 12. Indemnification + +12.1 To the maximum extent permitted by law, you agree to defend, indemnify and hold harmless Google, its affiliates and their respective directors, officers, employees and agents from and against any and all claims, actions, suits or proceedings, as well as any and all losses, liabilities, damages, costs and expenses (including reasonable attorneys fees) arising out of or accruing from (a) your use of the SDK, (b) any application you develop on the SDK that infringes any copyright, trademark, trade secret, trade dress, patent or other intellectual property right of any person or defames any person or violates their rights of publicity or privacy, and (c) any non-compliance by you with the License Agreement. + +### 13. Changes to the License Agreement + +13.1 Google may make changes to the License Agreement as it distributes new versions of the SDK. When these changes are made, Google will make a new version of the License Agreement available on the website where the SDK is made available. + +### 14. General Legal Terms + +14.1 The License Agreement constitutes the whole legal agreement between you and Google and governs your use of the SDK (excluding any services which Google may provide to you under a separate written agreement), and completely replaces any prior agreements between you and Google in relation to the SDK. 14.2 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the License Agreement (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google. 14.3 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of the License Agreement is invalid, then that provision will be removed from the License Agreement without affecting the rest of the License Agreement. The remaining provisions of the License Agreement will continue to be valid and enforceable. 14.4 You acknowledge and agree that each member of the group of companies of which Google is the parent will be third party beneficiaries to the License Agreement and that such other companies will be entitled to directly enforce, and rely upon, any provision of the License Agreement that confers a benefit on (or rights in favor of) them. Other than this, no other person or company will be third party beneficiaries to the License Agreement. 14.5 EXPORT RESTRICTIONS. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON DESTINATIONS, END USERS AND END USE. 14.6 The rights granted in the License Agreement may not be assigned or transferred by either you or Google without the prior written approval of the other party. Neither you nor Google will be permitted to delegate their responsibilities or obligations under the License Agreement without the prior written approval of the other party. 14.7 The License Agreement, and your relationship with Google under the License Agreement, will be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google will still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction. *April 28, 2026* I have read and agree with the above terms and conditions [Download](https://dl.google.com/developers/android/channels/verify_app_multiplatform_public_20251215.apk) + +*verify_app_multiplatform_public_20251215.apk* \ No newline at end of file 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 new file mode 100644 index 0000000..478747d --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md @@ -0,0 +1,471 @@ +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. + +## Pre-work + +> [!IMPORTANT] +> **Important:** [Express interest in developing the Video Discovery API](http://g.co/tv/vda). + +Complete the [Pre-work](https://developer.android.com/guide/playcore/engage/tv/getting-started#pre-work) instructions in the Getting Started guide. + +1. Execute publishing on a foreground service. +2. Publish recommendations data at most once daily, triggered by either of + - User's first login of the day. (*or*) + - When the user starts interacting with the application. + +## Integration + +`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). + + 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. + +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 + .Builder() + .addEntity(movie1) + .addEntity(movie2) + .addEntity(movie3) + .addEntity(movie4) + .addEntity(tvShow) + // This cluster is meant to be used as an individual provider row + .setRecommendationClusterType(TYPE_PROVIDER_ROW) + .setTitle("Popular Movies") + .build() + + // cluster for live TV programs + val recommendationCluster2 = RecommendationCluster + .Builder() + .addEntity(liveTvProgramEntity1) + .addEntity(liveTvProgramEntity2) + .addEntity(liveTvProgramEntity3) + .addEntity(liveTvProgramEntity4) + .addEntity(liveTvProgramEntity5) + // This cluster is meant to be used as an individual provider row + .setRecommendationClusterType(TYPE_PROVIDER_ROW) + .setTitle("Popular Live TV Programs") + .build() + + // creating a publishing request + val recommendationRequest = PublishRecommendationClustersRequest + .Builder() + .setSyncAcrossDevices(true) + .setAccountProfile(accountProfile) + .addRecommendationCluster(recommendationCluster1) + .addRecommendationCluster(recommendationCluster2) + .build() + +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. + +> [!NOTE] +> **Note:** Publish APIs are upsert operations, replacing existing content; update entities by republishing the entire cluster. Only publish recommendations for adult accounts. Avoid using delete APIs followed by publish, as the latter inherently replaces content. + +### 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. + +### 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`. + +### Delete the video discovery 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 video discovery +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. + + // 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, + // the recommendations data persists on the current Google TV device until 60 + // days later. + client.deleteClusters( + new DeleteClustersRequest.Builder() + .setAccountProfile(AccountProfile()) + .setReason(DeleteReason.DELETE_REASON_USER_LOG_OUT) + .build() + ) + + // If the user revokes the consent to share data with Google TV, you must make + // the following call to remove recommendations data from all current Google TV + // devices. Otherwise, the recommendations data persists until 60 days later. + client.deleteClusters( + new DeleteClustersRequest.Builder() + .setAccountProfile(AccountProfile()) + .setReason(DeleteReason.DELETE_REASON_LOSS_OF_CONSENT) + .build() + ) + +## Create entities + +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) +3. [`TvShowEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/TvShowEntity) +4. [`LiveTvChannelEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/LiveTvChannelEntity) +5. [`LiveTvProgramEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/LiveTvProgramEntity) + +### 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. + +### 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. + +### Tags + +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. + +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 + val playbackUriTv = PlatformSpecificUri + .Builder() + .setPlatformType(PlatformType.TYPE_ANDROID_TV) + .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_tv")) + .build() + + // Optional. Set this when you want recommended entities to show up on + // Google TV Android app + val playbackUriAndroid = PlatformSpecificUri + .Builder() + .setPlatformType(PlatformType.TYPE_ANDROID_MOBILE) + .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_android")) + .build() + + // Optional. Set this when you want recommended entities to show up on + // Google TV iOS app + val playbackUriIos = PlatformSpecificUri + .Builder() + .setPlatformType(PlatformType.TYPE_IOS) + .setActionUri(Uri.parse("https://www.example.com/entity_uri_for_ios")) + .build() + + val platformSpecificPlaybackUris = + Arrays.asList(playbackUriTv, playbackUriAndroid, playbackUriIos) + + // Provide appropriate rating for the system. + val contentRating = new RatingSystem + .Builder() + .setAgencyName("MPAA") + .setRating("PG-13") + .build() + +### 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. + + Image image1 = new Image.Builder() + .setImageUri(Uri.parse("http://www.example.com/entity_image1.png");) + .setImageHeightInPixel(300) + .setImageWidthInPixel(169) + .build() + + Image image2 = new Image.Builder() + .setImageUri(Uri.parse("http://www.example.com/entity_image2.png");) + .setImageHeightInPixel(640) + .setImageWidthInPixel(360) + .build() + + // And other images for different form factors. + val images = Arrays.asList(image1, image2) + +### 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. + + //Allows us to construct reason: "Because it is top 10 on your Channel" + val topOnPartner = RecommendationReasonTopOnPartner + .Builder() + .setNum(10) //any valid integer value + .build() + + //Allows us to construct reason: "Because it is popular on your Channel" + val popularOnPartner = RecommendationReasonPopularOnPartner + .Builder() + .build() + + //Allows us to construct reason: "New to your channel, or Just added" + val newOnPartner = RecommendationReasonNewOnPartner + .Builder() + .build() + + //Allows us to construct reason: "Because you watched Star Wars" + val watchedSimilarTitles = RecommendationReasonWatchedSimilarTitles + .addSimilarWatchedTitleName("Movie or TV Show Title") + .addSimilarWatchedTitleName("Movie or TV Show Title") + .Builder() + .build() + + //Allows us to construct reason: "Recommended for you by ChannelName" + val recommendedForUser = RecommendationReasonRecommendedForUser + .Builder() + .build() + + val watchAgain = RecommendationReasonWatchAgain + .Builder() + .build() + + val fromUserWatchList = RecommendationReasonFromUserWatchlist + .Builder() + .build() + + val userLikedOnPartner = RecommendationReasonUserLikedOnPartner + .Builder() + .setTitleName("Movie or TV Show Title") + .build() + + val generic = RecommendationReasonGeneric.Builder().build() + +### 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. + + val window1 = DisplayTimeWindow + .Builder() + .setStartTimeStampMillis(now()+ 1.days.toMillis()) + .setEndTimeStampMillis(now()+ 30.days.toMillis()) + + val window2 = DisplayTimeWindow + .Builder() + .setEndTimeStampMillis(now()+ 30.days.toMillis()) + + val availabilityTimeWindows: List = listof(window1,window2) + +### 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. + + val id = "dataFeedElementId" + +### `MovieEntity` + +Here's an example of creating a `MovieEntity` with all the required fields: + + val movieEntity = MovieEntity.Builder() + .setName("Movie name") + .setDescription("A sentence describing movie.") + .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris) + .addPosterImages(images) + // Suppose the duration is 2 hours, it is 72000000 in milliseconds + .setDurationMills(72000000) + .setCallToActionText("Watch Now") + .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. + + val genres = Arrays.asList("Action", "Science fiction"); + val rating1 = RatingSystem.Builder().setAgencyName("MPAA").setRating("pg-13").build(); + val contentRatings = Arrays.asList(rating1); + //Suppose release date is 11-02-2025 + val releaseDate = 1739233800000L + val movieEntity = MovieEntity.Builder() + ... + .addGenres(genres) + .setReleaseDateEpochMillis(releaseDate) + .addContentRatings(contentRatings) + .setRecommendationReason(topOnPartner or watchedSimilarTitles) + .addAllAvailabilityTimeWindows(availabilityTimeWindows) + .build() + +### `TvShowEntity` + +Here's an example of creating a `TvShowEntity` with all the required fields: + + val tvShowEntity = TvShowEntity.Builder() + .setName("Show title") + .setDescription("A sentence describing TV Show.") + .addPlatformSpecificPlaybackUri(platformSpecificPlaybackUris) + .addPosterImages(images) + .setCallToActionText("Watch Now") + .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. + + val genres = Arrays.asList("Action", "Science fiction"); + val rating1 = RatingSystem.Builder() + .setAgencyName("MPAA") + .setRating("pg-13") + .build(); + val price = Price.Builder() + .setCurrentPrice("$14.99") + .setStrikethroughPrice("$16.99") + .build(); + val contentRatings = Arrays.asList(rating1); + val seasonCount = 5; + val tvShowEntity = TvShowEntity.Builder() + ... + .addGenres(genres) + .addContentRatings(contentRatings) + .setRecommendationReason(topOnPartner or watchedSimilarTitles) + .addAllAvailabilityTimeWindows(availabilityTimeWindows) + .setSeasonCount(seasonCount) + .setPrice(price) + .build() + +### `MediaActionFeedEntity` + +Here's an example of creating an `MediaActionFeedEntity` with all the required +fields: + + val mediaActionFeedEntity = MediaActionFeedEntity.Builder() + .setDataFeedElementId(id) + .setCallToActionText("Watch Now") + .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. + + val mediaActionFeedEntity = MediaActionFeedEntity.Builder() + .setName("Movie name or TV Show name") + .setDescription("A sentence describing an entity") + .setRecommendationReason(topOnPartner or watchedSimilarTitles) + .addPosterImages(images) + .build() + +### `LiveTvChannelEntity` + +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") + // ID of the live TV channel + .setEntityId("https://www.example.com/channel/12345") + .setDescription("A sentence describing this live TV channel.") + // channel playback uri must contain at least PlatformType.TYPE_ANDROID_TV + .addPlatformSpecificPlaybackUri(channelPlaybackUris) + .addLogoImage(logoImage) + .setCallToActionText("Watch Now") + .addTag("News") + .build() + +Optionally provide additional data such as content ratings or +recommendation reason. + + val rating1 = RatingSystem.Builder() + .setAgencyName("MPAA") + .setRating("pg-13") + .build() + val contentRatings = Arrays.asList(rating1) + + val liveTvChannelEntity = LiveTvChannelEntity.Builder() + ... + .addContentRatings(contentRatings) + .setRecommendationReason(topOnPartner) + .build() + +### `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: + + val liveTvProgramEntity = LiveTvProgramEntity.Builder() + // First set the channel information + .setChannelName("Channel Name") + .setChannelId("https://www.example.com/channel/12345") + // channel playback uri must contain at least PlatformType.TYPE_ANDROID_TV + .addPlatformSpecificPlaybackUri(channelPlaybackUris) + .setChannelLogoImage(channelLogoImage) + // Then set the program or card specific information. + .setName("Program Name") + .setEntityId("https://www.example.com/schedule/123") + .setDescription("Program Description") + .addAvailabilityTimeWindow( + DisplayTimeWindow.Builder() + .setStartTimestampMillis(1756713600000L)// 2025-09-01T07:30:00+0000 + .setEndTimestampMillis(1756715400000L))// 2025-09-01T08:00:00+0000 + .addPosterImage(programImage) + .setCallToActionText("Watch Now") + .addTag("Sports") + .build() + +Optionally provide additional data such as content ratings, genres, or +recommendation reason. + + val rating1 = RatingSystem.Builder() + .setAgencyName("MPAA") + .setRating("pg-13") + .build() + val contentRatings = Arrays.asList(rating1) + val genres = Arrays.asList("Action", "Science fiction") + + val liveTvProgramEntity = LiveTvProgramEntity.Builder() + ... + .addContentRatings(contentRatings) + .addGenres(genres) + .setRecommendationReason(topOnPartner) + .build() + +> [!NOTE] +> **Note:** If the additional fields are provided directly within the entity, these values will be prioritized. The locale, if provided in the Account Profile, will be used to fetch the remaining entity metadata from Google's database in the language consistent with the request. \ No newline at end of file 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 new file mode 100644 index 0000000..d910945 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md @@ -0,0 +1,978 @@ +> [!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)**. + +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. + +## Integration detail + +### Terminology + +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. + + Your recommendations take the following structure: + - **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. + + ![](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. + + ![](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. + + ![](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). + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.5.12' + } + +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 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 | +|---|---|---| +| Recommendation Cluster(s) | At most 7 | At most 50 | +| Continuation Cluster | At most 1 | At most 20 | +| 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). + +### 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: + +| MediaHomeVideoContract integration equivalent | Engage SDK integration equivalent | +|---|---| +| `com.google.android.mediahome.video.PreviewChannel` | ` com.google.android.engage.common.datamodel.RecommendationCluster ` | +| `com.google.android.mediahome.video.PreviewChannel.Builder` | ` com.google.android.engage.common.datamodel.RecommendationCluster.Builder ` | +| `com.google.android.mediahome.video.PreviewChannelHelper` | `com.google.android.engage.video.service.AppEngageVideoClient` | +| `com.google.android.mediahome.video.PreviewProgram` | Divided into separate classes: `EventVideo`, `LiveStreamingVideo`, `Movie`, `TvEpisode`, `TvSeason`, `TvShow`, `VideoClipEntity` | +| `com.google.android.mediahome.video.PreviewProgram.Builder` | Divided into builders in separate classes: `EventVideo`, `LiveStreamingVideo`, `Movie`, `TvEpisode`, `TvSeason`, `TvShow`, `VideoClipEntity` | +| `com.google.android.mediahome.video.VideoContract` | No longer needed. | +| `com.google.android.mediahome.video.WatchNextProgram` | Divided into attributes in separate classes: `EventVideoEntity`, `LiveStreamingVideoEntity`, `MovieEntity`, `TvEpisodeEntity`, `TvSeasonEntity`, `TvShowEntity`, `VideoClipEntity` | +| `com.google.android.mediahome.video.WatchNextProgram.Builder` | Divided into attributes in separate classes: `EventVideoEntity`, `LiveStreamingVideoEntity`, `MovieEntity`, `TvEpisodeEntity`, `TvSeasonEntity`, `TvShowEntity`, `VideoClipEntity` | + +#### Publishing clusters in Media Home SDK vs Engage SDK + +With Media Home SDK, clusters and entities were published through separate APIs: + + // 1. Fetch existing channels + List channels = PreviewChannelHelper.getAllChannels(); + + // 2. If there are no channels, publish new channels + long channelId = PreviewChannelHelper.publishChannel(builder.build()); + + // 3. If there are existing channels, decide whether to update channel contents + PreviewChannelHelper.updatePreviewChannel(channelId, builder.build()); + + // 4. Delete all programs in the channel + PreviewChannelHelper.deleteAllPreviewProgramsByChannelId(channelId); + + // 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: + +### Kotlin + + RecommendationCluster.Builder() + .addEntity(MOVIE_ENTITY) + .addEntity(MOVIE_ENTITY) + .addEntity(MOVIE_ENTITY) + .setTitle("Top Picks For You") + .build() + +### Java + + new RecommendationCluster.Builder() + .addEntity(MOVIE_ENTITY) + .addEntity(MOVIE_ENTITY) + .addEntity(MOVIE_ENTITY) + .setTitle("Top Picks For You") + .build(); + +### 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: + +1. [`MovieEntity`](https://developer.android.com/guide/playcore/engage/watch#movieentity) +2. [`TvShowEntity`](https://developer.android.com/guide/playcore/engage/watch#tvshowentity) +3. [`TvSeasonEntity`](https://developer.android.com/guide/playcore/engage/watch#tvseasonentity) +4. [`TvEpisodeEntity`](https://developer.android.com/guide/playcore/engage/watch#tvepisodeentity) +5. [`LiveStreamingVideoEntity`](https://developer.android.com/guide/playcore/engage/watch#livestreamingvideoentity) +6. [`VideoClipEntity`](https://developer.android.com/guide/playcore/engage/watch#videoclipentity) + +The following chart outlines attributes and requirements for each type. + +#### `MovieEntity` + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Playback uri | **Required** | The deep link to the provider app to start playing the movie. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Info page uri | Optional | The deep link to the provider app to show details about the movie. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Release date | Optional | In epoch milliseconds. | +| Availability | **Required** | AVAILABLE: The content is available to the user without any further action. FREE_WITH_SUBSCRIPTION: The content is available after the user purchases a subscription. PAID_CONTENT: The content requires user purchase or rental. PURCHASED: The content has been purchased or rented by the user. | +| Offer price | Optional | Free text | +| Duration | **Required** | In milliseconds. | +| Genre | **Required** | Free text | +| Content ratings | Optional | Free text, follow the industry standard. ([Example](https://www.spectrum.net/support/tv/tv-and-movie-ratings-descriptions)) | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### `TvShowEntity` + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Info page uri | **Required** | The deep link to the provider app to show the details of the TV show. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Playback uri | Optional | The deep link to the provider app to start playing the TV show. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| First episode air date | Optional | In epoch milliseconds. | +| Latest episode air date | Optional | In epoch milliseconds. | +| Availability | **Required** | AVAILABLE: The content is available to the user without any further action. FREE_WITH_SUBSCRIPTION: The content is available after the user purchases a subscription. PAID_CONTENT: The content requires user purchase or rental. PURCHASED: The content has been purchased or rented by the user. | +| Offer price | Optional | Free text | +| Season count | **Required** | Positive integer | +| Genre | **Required** | Free text | +| Content ratings | Optional | Free text, follow the industry standard. ([Example](https://www.spectrum.net/support/tv/tv-and-movie-ratings-descriptions)) | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### `TvSeasonEntity` + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Info page uri | **Required** | The deep link to the provider app to show the details of the TV show season. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Playback uri | Optional | The deep link to the provider app to start playing the TV show season. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Display Season number | **Optional** **Available in v1.3.1** | String | +| First episode air date | Optional | In epoch milliseconds. | +| Latest episode air date | Optional | In epoch milliseconds. | +| Availability | **Required** | AVAILABLE: The content is available to the user without any further action. FREE_WITH_SUBSCRIPTION: The content is available after the user purchases a subscription. PAID_CONTENT: The content requires user purchase or rental. PURCHASED: The content has been purchased or rented by the user. | +| Offer price | Optional | Free text | +| Episode count | **Required** | Positive integer | +| Genre | **Required** | Free text | +| Content ratings | Optional | Free text, follow the industry standard. ([Example](https://www.spectrum.net/support/tv/tv-and-movie-ratings-descriptions)) | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### `TvEpisodeEntity` + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Playback uri | **Required** | The deep link to the provider app to start playing the episode. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Info page uri | Optional | The deep link to the provider app to show details about the TV show episode. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Display Episode number | **Optional** **Available in v1.3.1** | String | +| Air date | **Required** | In epoch milliseconds. | +| Availability | **Required** | AVAILABLE: The content is available to the user without any further action. FREE_WITH_SUBSCRIPTION: The content is available after the user purchases a subscription. PAID_CONTENT: The content requires user purchase or rental. PURCHASED: The content has been purchased or rented by the user. | +| Offer price | Optional | Free text | +| Duration | **Required** | Must be a positive value in milliseconds. | +| Genre | **Required** | Free text | +| Content ratings | Optional | Free text, follow the industry standard. ([Example](https://www.spectrum.net/support/tv/tv-and-movie-ratings-descriptions)) | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### `LiveStreamingVideoEntity` + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Playback uri | **Required** | The deep link to the provider app to start playing the video. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Broadcaster | **Required** | Free text | +| Start time | Optional | In epoch milliseconds. | +| End time | Optional | In epoch milliseconds. | +| View count | Optional | Free text, must be localized. | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### `VideoClipEntity` + +The `VideoClipEntity` object represents a video entity coming from social media, +such as TikTok or YouTube. + +| Attribute | Requirement | Notes | +|---|---|---| +| Name | **Required** | | +| Poster images | **Required** | At least one image is required, and must be provided with an aspect ratio. (Landscape is preferred but passing both portrait and landscape images for different scenarios is recommended.) See [Image Specifications](https://developer.android.com/guide/playcore/engage/watch#image-specs) for guidance. | +| Playback uri | **Required** | The deep link to the provider app to start playing the video. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | +| Created time | **Required** | In epoch milliseconds. | +| Duration | **Required** | Must be a positive value in milliseconds. | +| Creator | **Required** | Free text | +| Creator image | Optional | Image of the Creator avatar | +| View count | Optional | Free text, must be localized. | +| Call to action text | Optional | Free text to be displayed as a call to action. | +| Tags | Optional | List of tags associated with the entity. | +| Watch next type | Conditionally required | Must be provided when the item is in the Continuation cluster and must be one of the following four types: CONTINUE: The user has already watched more than 1 minute of this content. NEW: The user has watched all available episodes from some episodic content, but a new episode has become available and there is exactly one unwatched episode. This works for TV shows, recorded soccer matches in a series, and so on. NEXT: The user has watched one or more complete episodes from some episodic content, but there remains either more than one episode remaining or exactly one episode remaining where the last episode is not "NEW" and was released before the user started watching the episodic content. WATCHLIST: The user has explicitly elected to add a movie, event, or series to a watchlist to manually curate what they want to watch next. | +| Last engagement time | Conditionally required | Must be provided when the item is in the Continuation cluster. In epoch milliseconds. | +| Last playback position time | Conditionally required | Must be provided when the item is in the Continuation cluster and WatchNextType is CONTINUE. In epoch milliseconds. | + +#### Image specifications + +The following section lists the required specifications for image assets: + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. + +#### Example + +### Kotlin + + var movie = MovieEntity.Builder() + .setName("Avengers") + .addPosterImage(Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(960) + .setImageWidthInPixel(408) + .build()) + .setPlayBackUri(Uri.parse("http://tv.com/playback/1")) + .setReleaseDateEpochMillis(1633032895L) + .setAvailability(ContentAvailability.AVAILABILITY_AVAILABLE) + .setDurationMillis(12345678L) + .addGenre("action") + .addContentRating("R") + .setWatchNextType(WatchNextType.TYPE_NEW) + .setLastEngagementTimeMillis(1664568895L) + .setCallToActionText("Watch Now") + .addTag("Action") + .build() + +### Java + + MovieEntity movie = new MovieEntity.Builder() + .setName("Avengers") + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(960) + .setImageWidthInPixel(408) + .build()) + .setPlayBackUri(Uri.parse("http://tv.com/playback/1")) + .setReleaseDateEpochMillis(1633032895L) + .setAvailability(ContentAvailability.AVAILABILITY_AVAILABLE) + .setDurationMillis(12345678L) + .addGenre("action") + .addContentRating("R") + .setWatchNextType(WatchNextType.TYPE_NEW) + .setLastEngagementTimeMillis(1664568895L) + .setCallToActionText("Watch Now") + .addTag("Action") + .build(); + +### 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). + +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishFeaturedCluster` +- `publishContinuationCluster` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteFeaturedCluster` +- `deleteContinuationCluster` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +This API is used to check if the service is available for integration and +whether the content can be presented on the device. + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list of `RecommendationCluster` objects. + +> [!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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build() + ) + .build() + ) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Top Picks For You") + .build()) + .build()); + +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. + +#### `publishFeaturedCluster` + +This API is used to publish a list of `FeaturedCluster` objects. + +> [!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. + +### Kotlin + + client.publishFeaturedCluster( + PublishFeaturedClusterRequest.Builder() + .setFeaturedCluster( + FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishFeaturedCluster( + new PublishFeaturedClustersRequest.Builder() + .addFeaturedCluster( + new FeaturedCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `publishContinuationCluster` + +This API is used to publish a `ContinuationCluster` object. + +> [!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. + +### Kotlin + + client.publishContinuationCluster( + PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()) + +### Java + + client.publishContinuationCluster( + new PublishContinuationClusterRequest.Builder() + .setContinuationCluster( + new ContinuationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteFeaturedCluster` + +This API is used to delete the content of Featured Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteFeaturedCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteContinuationCluster` + +This API is used to delete the content of Continuation Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteContinuationCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +> [!NOTE] +> **Note:** This api is available from version 1.1.0 onwards. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_CONTINUATION) + .addClusterType(ClusterType.TYPE_FEATURED) + .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. + +#### 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(..) + .build()) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // do something + } else { + val exception = task.exception + if (exception is AppEngageException) { + @AppEngageErrorCode val errorCode = exception.errorCode + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + } + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Featured Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + + // Register Continuation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + + // Trigger featured cluster publish when PUBLISH_FEATURED broadcast is received + + // Trigger continuation cluster publish when PUBLISH_CONTINUATION broadcast is + // received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents. ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Featured Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null); + + // Register Continuation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + + + + + + + +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. +- `com.google.android.engage.action.PUBLISH_CONTINUATION` It is recommended to start a `publishContinuationCluster` 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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation** , **Featured** , and **Continuation** clusters may be published and visible to users. \ No newline at end of file diff --git a/play/engage-sdk-integration/references/clusters.md b/play/engage-sdk-integration/references/clusters.md new file mode 100644 index 0000000..6791095 --- /dev/null +++ b/play/engage-sdk-integration/references/clusters.md @@ -0,0 +1,149 @@ +This file defines the structure of various clusters in the Engage SDK. + + { + "clusters": { + "FeaturedCluster": { + "package": "com.google.android.engage.common.datamodel.FeaturedCluster", + "fields": { + "entities": { + "type": "List", + "requirement": "Required", + "adder": "addEntity(Entity)", + "getter": "getEntities()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "ContinuationCluster": { + "package": "com.google.android.engage.common.datamodel.ContinuationCluster", + "fields": { + "syncAcrossDevices": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setSyncAcrossDevices(boolean)", + "getter": "getSyncAcrossDevices()" + }, + "accountProfile": { + "type": "AccountProfile", + "requirement": "Optional", + "setter": "setAccountProfile(AccountProfile)", + "getter": "getAccountProfile()" + }, + "entities": { + "type": "List", + "requirement": "Required", + "adder": "addEntity(Entity)", + "getter": "getEntities()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "RecommendationCluster": { + "package": "com.google.android.engage.common.datamodel.RecommendationCluster", + "fields": { + "entities": { + "type": "List", + "requirement": "Required", + "adder": "addEntity(Entity)", + "getter": "getEntities()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "subtitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSubtitle(String)", + "getter": "getSubtitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "recommendationClusterType": { + "type": "@RecommendationClusterType int", + "requirement": "Optional", + "setter": "setRecommendationClusterType(@RecommendationClusterType int)", + "getter": "getRecommendationClusterType()" + } + } + }, + "SubscriptionCluster": { + "package": "com.google.android.engage.common.datamodel.SubscriptionCluster", + "fields": { + "accountProfile": { + "type": "AccountProfile", + "requirement": "Optional", + "setter": "setAccountProfile(AccountProfile)", + "getter": "getAccountProfile()" + }, + "subscriptionEntities": { + "type": "List", + "requirement": "Required", + "adder": "addSubscriptionEntity(SubscriptionEntity)", + "getter": "getSubscriptionEntities()" + } + } + }, + "EngagementCluster": { + "package": "com.google.android.engage.common.datamodel.EngagementCluster", + "fields": { + "signInCardEntity": { + "type": "SignInCardEntity", + "requirement": "Required", + "setter": "setSignInCardEntity(SignInCardEntity)" + }, + "userSettingsCardEntity": { + "type": "UserSettingsCardEntity", + "requirement": "Required", + "setter": "setUserSettingsCardEntity(UserSettingsCardEntity)" + } + } + } + } + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/common.md b/play/engage-sdk-integration/references/common.md new file mode 100644 index 0000000..52c3d5e --- /dev/null +++ b/play/engage-sdk-integration/references/common.md @@ -0,0 +1,519 @@ +This file defines common data models and entities used across the Engage SDK. + + { + "entities": { + "Image": { + "package": "com.google.android.engage.common.datamodel.Image", + "fields": { + "imageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setImageUri(Uri)", + "getter": "getImageUri()" + }, + "height": { + "type": "Integer", + "requirement": "Optional" + }, + "width": { + "type": "Integer", + "requirement": "Optional" + }, + "accessibilityText": { + "type": "String", + "requirement": "Required", + "setter": "setAccessibilityText(String)", + "getter": "getAccessibilityText()" + }, + "theme": { + "type": "ImageThemeEnum", + "requirement": "Optional" + }, + "cropType": { + "type": "ImageCropTypeEnum", + "requirement": "Optional" + }, + "imageHeightInPixel": { + "requirement": "Required", + "setter": "setImageHeightInPixel(int)", + "type": "Integer", + "getter": "getImageHeightInPixel()" + }, + "imageWidthInPixel": { + "requirement": "Required", + "setter": "setImageWidthInPixel(int)", + "type": "Integer", + "getter": "getImageWidthInPixel()" + }, + "imageTheme": { + "requirement": "Optional", + "setter": "setImageTheme(@ImageTheme int)", + "type": "@ImageTheme int", + "getter": "getImageTheme()" + }, + "imageCropType": { + "requirement": "Optional", + "setter": "setImageCropType(@ImageCropType int)", + "type": "@ImageCropType int", + "getter": "getImageCropType()" + } + } + }, + "Price": { + "package": "com.google.android.engage.common.datamodel.Price", + "fields": { + "currentPrice": { + "type": "String", + "requirement": "Required", + "setter": "setCurrentPrice(String)", + "getter": "getCurrentPrice()" + }, + "strikethroughPrice": { + "type": "String", + "requirement": "Optional", + "setter": "setStrikethroughPrice(String)", + "getter": "getStrikethroughPrice()" + } + } + }, + "Rating": { + "package": "com.google.android.engage.common.datamodel.Rating", + "fields": { + "ratingValue": { + "type": "Float", + "requirement": "Required" + }, + "maxValue": { + "type": "Double", + "requirement": "Required", + "setter": "setMaxValue(double)", + "getter": "getMaxValue()" + }, + "ratingCount": { + "type": "Integer", + "requirement": "Optional" + }, + "ratingCountValue": { + "type": "String", + "requirement": "Optional" + }, + "currentValue": { + "requirement": "Required", + "setter": "setCurrentValue(double)", + "type": "Double", + "getter": "getCurrentValue()" + }, + "count": { + "requirement": "Optional", + "setter": "setCount(String)", + "type": "String", + "getter": "getCount()" + }, + "countValue": { + "requirement": "Optional", + "setter": "setCountValue(long)", + "type": "Long", + "getter": "getCountValue()" + } + } + }, + "DisplayTimeWindow": { + "package": "com.google.android.engage.common.datamodel.DisplayTimeWindow", + "fields": { + "startTimeMillis": { + "type": "Long", + "requirement": "Required" + }, + "endTimeMillis": { + "type": "Long", + "requirement": "Required" + }, + "startTimestampMillis": { + "requirement": "Optional", + "setter": "setStartTimestampMillis(long)", + "type": "Long", + "getter": "getStartTimestampMillis()" + }, + "endTimestampMillis": { + "requirement": "Optional", + "setter": "setEndTimestampMillis(long)", + "type": "Long", + "getter": "getEndTimestampMillis()" + } + } + }, + "AccountProfile": { + "package": "com.google.android.engage.common.datamodel.AccountProfile", + "fields": { + "accountId": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setAccountId(@NonNull String)", + "getter": "getAccountId()" + }, + "profileId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setProfileId(@NonNull String)", + "getter": "getProfileId()" + }, + "accountName": { + "type": "String", + "requirement": "Optional" + }, + "profileImage": { + "type": "Image", + "requirement": "Optional" + }, + "locale": { + "requirement": "Optional", + "setter": "setLocale(@NonNull String)", + "type": "@NonNull String", + "getter": "getLocale()" + } + } + }, + "Address": { + "package": "com.google.android.engage.common.datamodel.Address", + "fields": { + "city": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setCity(@NonNull String)", + "getter": "getCity()" + }, + "country": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setCountry(@NonNull String)", + "getter": "getCountry()" + }, + "displayAddress": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setDisplayAddress(@NonNull String)", + "getter": "getDisplayAddress()" + }, + "streetAddress": { + "type": "String", + "requirement": "Optional", + "setter": "setStreetAddress(String)", + "getter": "getStreetAddress()" + }, + "state": { + "type": "String", + "requirement": "Optional", + "setter": "setState(String)", + "getter": "getState()" + }, + "zipCode": { + "type": "String", + "requirement": "Optional", + "setter": "setZipCode(String)", + "getter": "getZipCode()" + }, + "neighborhood": { + "type": "String", + "requirement": "Optional", + "setter": "setNeighborhood(String)", + "getter": "getNeighborhood()" + } + } + }, + "Badge": { + "package": "com.google.android.engage.common.datamodel.Badge", + "fields": { + "text": { + "type": "String", + "requirement": "Optional", + "setter": "setText(String)", + "getter": "getText()" + }, + "image": { + "type": "Image", + "requirement": "Optional", + "setter": "setImage(Image)", + "getter": "getImage()" + } + } + }, + "LocalizedTimestamp": { + "package": "com.google.android.engage.common.datamodel.LocalizedTimestamp", + "fields": { + "timestamp": { + "type": "Instant", + "requirement": "Required", + "setter": "setTimestamp(Instant)", + "getter": "getTimestamp()" + }, + "timezone": { + "type": "DateTimeZone", + "requirement": "Required", + "setter": "setTimezone(DateTimeZone)", + "getter": "getTimezone()" + } + } + }, + "SignInCardEntity": { + "package": "com.google.android.engage.common.datamodel.SignInCardEntity", + "fields": { + "actionText": { + "type": "String", + "requirement": "Required", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "subtitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSubtitle(String)", + "getter": "getSubtitle()" + }, + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + } + } + }, + "GenericFeaturedEntity": { + "package": "com.google.android.engage.common.datamodel.GenericFeaturedEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "SubscriptionEntity": { + "package": "com.google.android.engage.common.datamodel.SubscriptionEntity", + "fields": {} + }, + "UserSettingsCardEntity": { + "package": "com.google.android.engage.common.datamodel.UserSettingsCardEntity", + "fields": { + "actionText": { + "type": "String", + "requirement": "Required", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "subtitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSubtitle(String)", + "getter": "getSubtitle()" + }, + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + } + } + }, + "ArticleEntity": { + "package": "com.google.android.engage.common.datamodel.ArticleEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "progressPercentage": { + "type": "Integer", + "requirement": "Required", + "setter": "setProgressPercentage(int)", + "getter": "getProgressPercentage()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastEngagementTimestampMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setLastEngagementTimestampMillis(long)", + "getter": "getLastEngagementTimestampMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "source": { + "type": "Badge", + "requirement": "Optional", + "setter": "setSource(Badge)", + "getter": "getSource()" + }, + "lastContentPublishTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastContentPublishTimestampMillis(Long)", + "getter": "getLastContentPublishTimestampMillis()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + } + }, + "intents": { + "ACTION_PUBLISH_RECOMMENDATION": "com.google.android.engage.action.PUBLISH_RECOMMENDATION", + "ACTION_PUBLISH_FEATURED": "com.google.android.engage.action.PUBLISH_FEATURED", + "ACTION_PUBLISH_CONTINUATION": "com.google.android.engage.action.PUBLISH_CONTINUATION" + }, + "imports": [ + "com.google.android.engage.service.AppEngagePublishClient", + "com.google.android.engage.service.AppEngageErrorCode", + "com.google.android.engage.service.AppEngageException", + "com.google.android.engage.service.AppEngagePublishStatusCode" + ] + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/patterns.md b/play/engage-sdk-integration/references/patterns.md new file mode 100644 index 0000000..bc7f214 --- /dev/null +++ b/play/engage-sdk-integration/references/patterns.md @@ -0,0 +1,447 @@ +## EngageBroadcastReceiver + +Setting up the `BroadcastReceiver` correctly requires **both** static and +dynamic registration. Static registration allows the app to receive broadcasts +even when it is not 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 +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import com.google.android.engage.service.BroadcastReceiverPermissions + +class EngageBroadcastReceiver : BroadcastReceiver() { + // IMPORTANT: Only trigger the specific publish job for the received intent action. + // DO NOT publish all clusters at once. + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null || context == null) return + when (intent.action) { + com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION + -> EngagePublisher.publishOneTime(context, Constants.PUBLISH_TYPE_RECOMMENDATIONS) + // Note: If app handles other publish actions (e.g. Featured, Continuation), add them here. + com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED + -> EngagePublisher.publishOneTime(context, Constants.PUBLISH_TYPE_FEATURED) + + /** Note: If vertical has other intents (e.g. FOOD shopping cart, etc.), add them here. + * com.google.android.engage.food.service.Intents.ACTION_PUBLISH_FOOD_SHOPPING_CART + * -> EngagePublisher.publishOneTime(context, Constants.PUBLISH_TYPE_FOOD_SHOPPING_CARD) + * com.google.android.engage.travel.service.Intents.ACTION_PUBLISH_RESERVATION + * -> EngagePublisher.publishOneTime(context, Constants.PUBLISH_TYPE_RESERVATION ) + **/ + } + } + + companion object { + /** + * Dynamically registers the receiver. + * This is required in addition to static registration in AndroidManifest.xml. + * Call this method in your Application's onCreate() or your main Activity's onCreate(). + */ + fun register(context: Context) { + val appContext = context.applicationContext + val receiver = EngageBroadcastReceiver() + + // Register Cluster Publish Intents + val filter = IntentFilter().apply { + addAction(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION) + addAction(com.google.android.engage.service.Intents.ACTION_PUBLISH_FEATURED) + addAction(com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION) + } + ContextCompat.registerReceiver( + appContext, + receiver, + filter, + BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + null, + ContextCompat.RECEIVER_EXPORTED + ) + // Note: Add vertical-specific intents here if applicable (e.g., FOOD shopping cart, etc.) + } + } +} +``` + +
+ +### Static Registration (AndroidManifest.xml) + +Add the `` tag inside the `` block in +`AndroidManifest.xml` + + +```kotlin + + + + + + + + + + +``` + +
+ +## EngageWorker + + +```kotlin +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.google.android.engage.service.AppEngageErrorCode +import com.google.android.engage.service.AppEngageException +import com.google.android.engage.service.AppEngagePublishClient +import com.google.android.engage.service.AppEngagePublishStatusCode +import com.google.android.engage.service.PublishStatusRequest +import com.google.android.gms.tasks.Task +import kotlinx.coroutines.tasks.await + +class EngageWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { + // Replace {AppEngagePublishClient} with the "client" class found in references/schemas/{VERTICAL}.md. + // Client class can vary based on app's vertical. + // Refer to the references/schemas/{VERTICAL}.md to find the right class. + // This is an example of using AppEngagePublishClient. + private val client = AppEngagePublishClient(context) + private val clusterRequestFactory = ClusterRequestFactory(context) + + override suspend fun doWork(): Result { + if (runAttemptCount > Constants.MAX_PUBLISHING_ATTEMPTS) { + // If we keep failing, report it as a service error before giving up. + updatePublishStatus(AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR) + return Result.failure() + } + + // Check if engage service is available before publishing. + val isAvailable = client.isServiceAvailable.await() + + // If the service is not available, do not attempt to publish and indicate failure. + if (!isAvailable) { + return Result.failure() + } + + val publishType = inputData.getString(Constants.PUBLISH_TYPE_KEY) + return when (publishType) { + Constants.PUBLISH_TYPE_RECOMMENDATIONS -> publishRecommendations() + // Constants.PUBLISH_TYPE_FEATURED -> publishFeatured() + // Constants.PUBLISH_TYPE_CONTINUATION-> publishContinuation() + Constants.PUBLISH_TYPE_USER_ACCOUNT_MANAGEMENT -> publishUserAccountManagement() + else -> Result.failure() + } + } + + // Use similar patterns for other clusters (Featured, Continuation, FoodShoppingList, Reservation etc.) + private suspend fun publishRecommendations(): Result { + val publishTask: Task = + client.publishRecommendationClusters( + clusterRequestFactory.constructRecommendationClustersRequest() + ) + return publishAndProvideResult(publishTask) + } + + private suspend fun publishUserAccountManagement(): Result { + val publishTask: Task + if (isAccountSignedIn()) { + // If signed in, we delete the sign-in card. + publishTask = client.deleteUserManagementCluster() + return publishAndProvideResult(publishTask) + } else { + // If not signed in, we publish the sign-in card. + // Note: Even though we are publishing a card, the status code is NOT_PUBLISHED_REQUIRES_SIGN_IN + // because the actual content (recommendations/continuation) is not published. + publishTask = + client.publishUserAccountManagementRequest( + clusterRequestFactory.constructUserAccountManagementClusterRequest() + ) + return try { + publishTask.await() + updatePublishStatus(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + Result.success() + } catch (publishException: Exception) { + handlePublishException(publishException) + } + } + } + + private fun isAccountSignedIn(): Boolean { + // Implement your app's sign-in check logic here. + // ... + } + + private suspend fun publishAndProvideResult( + publishTask: Task + ): Result { + return try { + // An AppEngageException may occur while publishing, so we may not be able to await the result. + publishTask.await() + // Update status to PUBLISHED only after successful publication. + updatePublishStatus(AppEngagePublishStatusCode.PUBLISHED) + Result.success() + } catch (publishException: Exception) { + handlePublishException(publishException) + } + } + + private fun handlePublishException(publishException: Exception): Result { + val appEngageException = publishException as? AppEngageException + if (appEngageException != null) { + logPublishing(appEngageException) + + // Map AppEngageException error codes to PublishStatusCodes + val errorStatusCode = when (appEngageException.errorCode) { + AppEngageErrorCode.SERVICE_CALL_INVALID_ARGUMENT -> + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR + + AppEngageErrorCode.SERVICE_CALL_PERMISSION_DENIED -> + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR + + else -> + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR + } + updatePublishStatus(errorStatusCode) + + // Some errors are recoverable, such as a threading issue, some are unrecoverable + // such as a cluster not containing all necessary fields. If an error is recoverable, we + // should attempt to publish again. Setting the result to retry means WorkManager will + // attempt to run the worker again, thus attempting to publish again. + return if (isErrorRecoverable(appEngageException)) Result.retry() else Result.failure() + } + return Result.failure() + } + + private fun updatePublishStatus(statusCode: Int) { + client + .updatePublishStatus(PublishStatusRequest.Builder().setStatusCode(statusCode).build()) + .addOnSuccessListener { + Log.i(TAG, "Successfully updated publish status code to $statusCode") + } + .addOnFailureListener { exception -> + Log.e(TAG, "Failed to update publish status code to $statusCode\n${exception.stackTrace}") + } + } + + private fun logPublishing(publishingException: AppEngageException) { + val message = when (publishingException.errorCode) { + AppEngageErrorCode.SERVICE_NOT_FOUND -> "Service not found" + AppEngageErrorCode.SERVICE_CALL_EXECUTION_FAILURE -> "Execution failure" + AppEngageErrorCode.SERVICE_NOT_AVAILABLE -> "Service not available" + AppEngageErrorCode.SERVICE_CALL_PERMISSION_DENIED -> "Permission denied" + AppEngageErrorCode.SERVICE_CALL_INVALID_ARGUMENT -> "Invalid argument" + AppEngageErrorCode.SERVICE_CALL_INTERNAL -> "Internal error" + AppEngageErrorCode.SERVICE_CALL_RESOURCE_EXHAUSTED -> "Resource exhausted" + else -> "Unknown error" + } + Log.d(TAG, message) + } + + private fun isErrorRecoverable(publishingException: AppEngageException): Boolean { + return when (publishingException.errorCode) { + // Recoverable Error codes + AppEngageErrorCode.SERVICE_CALL_EXECUTION_FAILURE, + AppEngageErrorCode.SERVICE_CALL_INTERNAL, + AppEngageErrorCode.SERVICE_CALL_RESOURCE_EXHAUSTED -> true + // Non recoverable error codes + AppEngageErrorCode.SERVICE_NOT_FOUND, + AppEngageErrorCode.SERVICE_CALL_INVALID_ARGUMENT, + AppEngageErrorCode.SERVICE_CALL_PERMISSION_DENIED, + AppEngageErrorCode.SERVICE_NOT_AVAILABLE -> false + else -> false + } + } +} +``` + +
+ +## ClusterRequestFactory + + +```kotlin +class ClusterRequestFactory(context: Context) { + + // ... + + private val signInCard = + com.google.android.engage.common.datamodel.SignInCardEntity.Builder() + .addPosterImage( + com.google.android.engage.common.datamodel.Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build() + ) + .setActionText(signInCardAction) + .setActionUri(Uri.parse("https://xyz.com/signin")) + .build() + + fun constructRecommendationClustersRequest(): com.google.android.engage.service.PublishRecommendationClustersRequest { + + val items = appDataRepository.getRecommendations() + val recommendationCluster = com.google.android.engage.common.datamodel.RecommendationCluster.Builder() + for (item in items) { + recommendationCluster.addEntity(ItemToEntityConverter.convert(item)) + } + return com.google.android.engage.service.PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(recommendationCluster.build()) + .build() + } + + fun constructUserAccountManagementClusterRequest(): com.google.android.engage.service.PublishUserAccountManagementRequest = + com.google.android.engage.service.PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(signInCard) + .build() +} +``` + +
+ +## EngagePublisher + + +```kotlin +object EngagePublisher { + + fun publishPeriodically(context: Context, publishType: String) { + val workRequest = PeriodicWorkRequestBuilder(Constants.REPEAT_INTERVAL, TimeUnit.HOURS) + .setInputData(workDataOf(Constants.PUBLISH_TYPE_KEY to publishType)) + .build() + WorkManager.getInstance(context).enqueueUniquePeriodicWork("EngagePeriodic", ExistingPeriodicWorkPolicy.KEEP, workRequest) + } + + fun publishOneTime(context: Context, publishType: String) { + + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(Constants.PUBLISH_TYPE_KEY to publishType)) + .build() + WorkManager.getInstance(context).enqueueUniqueWork("EngageOneTime", ExistingWorkPolicy.REPLACE, workRequest) + } +} +``` + +
+ +## Constants + + +```kotlin +object Constants { + // Holds common values like attempt counts, publish types etc. + const val REPEAT_INTERVAL = 24L + const val MAX_PUBLISHING_ATTEMPTS = 3 + const val PUBLISH_TYPE_KEY = "PUBLISH_TYPE" + const val PUBLISH_TYPE_RECOMMENDATIONS = "RECOMMENDATIONS" + const val PUBLISH_TYPE_FEATURED = "FEATURED" + // const val PUBLISH_TYPE_CONTINUATION = "CONTINUATION" + // ... + const val PUBLISH_TYPE_USER_ACCOUNT_MANAGEMENT = "USER_ACCOUNT_MANAGEMENT" + // const val PUBLISH_TYPE_FOOD_SHOPPING_CARD = "FOOD_SHOPPING_CARD" + // const val PUBLISH_TYPE_RESERVATION = "RESERVATION" +} +``` + +
+ +## ItemToEntityConverter + + +```kotlin +object ItemToEntityConverter { + // Converts app's local models to appropriate engage entity models. + // Use `{VERTICAL}.md` in the `references/schemas/` directory to identify the correct Engage entities. + // This is an example of using EbookEntity model. + fun convert(item: AppData): EbookEntity { + return EbookEntity.Builder() + // Implement required data mapping logic here. + .setName(item.title) + .addAuthor(item.author) + .build() + } +} +``` + +
+ +## Dependency Specifications (libs.versions.toml) + +This skill specifies all dependencies following in `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.5.12" + engage-tv = "1.0.6" + playServicesOssLicenses = "17.5.1" + workManager = "2.11.2" + coroutines = "1.10.2" + + [libraries] + engage-core = { group = "com.google.android.engage", name = "engage-core", version.ref = "engage-core" } + engage-tv = { group = "com.google.android.engage", name = "engage-tv", version.ref = "engage-tv" } + play-services-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "playServicesOssLicenses" } + androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" } + androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "workManager" } + kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } + kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutines" } + kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } + +## TV Integrations + +The following patterns and configurations are specific to Android TV +integrations. + +### AndroidManifest.xml (TV) + + +```kotlin + + +``` + +
+ +### PlatformSpecificUri Example + + +```kotlin +val platformSpecificPlaybackUris = listOf( + com.google.android.engage.common.datamodel.PlatformSpecificUri.Builder() + .setPlatformType(com.google.android.engage.common.datamodel.PlatformType.TYPE_ANDROID_TV) + .setActionUri(Uri.parse("https://www.example.com/tv/play/123")) + .build(), + com.google.android.engage.common.datamodel.PlatformSpecificUri.Builder() + .setPlatformType(com.google.android.engage.common.datamodel.PlatformType.TYPE_ANDROID_MOBILE) + .setActionUri(Uri.parse("https://www.example.com/mobile/play/123")) + .build() +) +``` + +
+ +### AccountProfile Example + + +```kotlin +val accountProfile: AccountProfile + get() = AccountProfile.Builder() + .setAccountId("user_123") + .setProfileId("profile_456") + // 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 new file mode 100644 index 0000000..c65f0a6 --- /dev/null +++ b/play/engage-sdk-integration/references/requests.md @@ -0,0 +1,231 @@ +This file defines the request structures for publishing various data models in +the Engage SDK. + + { + "PublishRecommendationClustersRequest": { + "package": "com.google.android.engage.service.PublishRecommendationClustersRequest", + "fields": { + "recommendationClusters": { + "type": "List", + "requirement": "Required", + "adder": "addRecommendationCluster(RecommendationCluster)", + "getter": "getRecommendationClusters()" + }, + "accountProfile": { + "type": "@NonNull AccountProfile", + "requirement": "Optional", + "setter": "setAccountProfile(@NonNull AccountProfile)", + "getter": "getAccountProfile()" + }, + "syncAcrossDevices": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setSyncAcrossDevices(boolean)", + "getter": "getSyncAcrossDevices()" + } + } + }, + "PublishFeaturedClusterRequest": { + "package": "com.google.android.engage.service.PublishFeaturedClusterRequest", + "fields": { + "featuredCluster": { + "type": "FeaturedCluster", + "requirement": "Required", + "setter": "setFeaturedCluster(FeaturedCluster)", + "getter": "getFeaturedCluster()" + } + } + }, + "DeleteClustersRequest": { + "package": "com.google.android.engage.service.DeleteClustersRequest", + "fields": { + "clusterTypes": { + "type": "List<@ClusterType int>", + "requirement": "Optional", + "adder": "addClusterType(@ClusterType int)" + }, + "deleteReason": { + "type": "@DeleteReason int", + "requirement": "Optional", + "setter": "setDeleteReason(@DeleteReason int)", + "getter": "getDeleteReason()" + }, + "accountProfile": { + "requirement": "Optional", + "setter": "setAccountProfile(AccountProfile)", + "type": "AccountProfile", + "getter": "getAccountProfile()" + }, + "syncAcrossDevices": { + "requirement": "Optional", + "setter": "setSyncAcrossDevices(boolean)", + "type": "Boolean", + "getter": "getSyncAcrossDevices()" + } + } + }, + "PublishContinuationClusterRequest": { + "package": "com.google.android.engage.service.PublishContinuationClusterRequest", + "fields": { + "continuationCluster": { + "type": "ContinuationCluster", + "requirement": "Required", + "setter": "setContinuationCluster(ContinuationCluster)", + "getter": "getContinuationCluster()" + } + } + }, + "PublishStatusRequest": { + "package": "com.google.android.engage.service.PublishStatusRequest", + "fields": { + "statusCode": { + "type": "@AppEngagePublishStatusCode int", + "requirement": "Required", + "setter": "setStatusCode(@AppEngagePublishStatusCode int)", + "getter": "getStatusCode()" + } + } + }, + "PublishSubscriptionRequest": { + "package": "com.google.android.engage.service.PublishSubscriptionRequest", + "fields": { + "subscriptionClusters": { + "type": "List", + "requirement": "Required" + }, + "accountProfile": { + "type": "AccountProfile", + "requirement": "Required", + "setter": "setAccountProfile(AccountProfile)", + "getter": "getAccountProfile()" + }, + "subscription": { + "requirement": "Required", + "setter": "setSubscription(SubscriptionEntity)", + "type": "SubscriptionEntity", + "getter": "getSubscription()" + } + } + }, + "PublishUserAccountManagementRequest": { + "package": "com.google.android.engage.service.PublishUserAccountManagementRequest", + "fields": { + "actionUri": { + "type": "Uri", + "requirement": "Required" + }, + "signInCardEntity": { + "type": "SignInCardEntity", + "requirement": "Required", + "setter": "setSignInCardEntity(SignInCardEntity)" + }, + "userSettingsCardEntity": { + "requirement": "Required", + "setter": "setUserSettingsCardEntity(UserSettingsCardEntity)", + "type": "UserSettingsCardEntity" + } + } + }, + "PublishShoppingCartClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingCartClusterRequest", + "fields": { + "shoppingCart": { + "requirement": "Required", + "setter": "setShoppingCart(ShoppingCart)", + "type": "ShoppingCart", + "getter": "getShoppingCart()" + } + } + }, + "PublishShoppingListsRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingListsRequest", + "fields": { + "shoppingLists": { + "type": "List", + "requirement": "Optional", + "adder": "addShoppingList(ShoppingList)", + "getter": "getShoppingLists()", + "adderAll": "addShoppingLists(List)" + } + } + }, + "PublishShoppingOrderTrackingClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingOrderTrackingClusterRequest", + "fields": { + "shoppingOrderTrackingCluster": { + "type": "ShoppingOrderTrackingCluster", + "requirement": "Required", + "setter": "setShoppingOrderTrackingCluster(ShoppingOrderTrackingCluster)", + "getter": "getShoppingOrderTrackingCluster()" + } + } + }, + "PublishShoppingReorderClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingReorderClusterRequest", + "fields": { + "reorderCluster": { + "type": "ShoppingReorderCluster", + "requirement": "Required", + "setter": "setReorderCluster(ShoppingReorderCluster)", + "getter": "getReorderCluster()" + } + } + }, + "PublishFoodShoppingCartsRequest": { + "package": "com.google.android.engage.food.service.PublishFoodShoppingCartsRequest", + "fields": { + "foodShoppingCarts": { + "type": "List", + "requirement": "Optional", + "adder": "addFoodShoppingCart(FoodShoppingCart)", + "getter": "getFoodShoppingCarts()", + "adderAll": "addFoodShoppingCarts(List)" + } + } + }, + "PublishFoodShoppingListsRequest": { + "package": "com.google.android.engage.food.service.PublishFoodShoppingListsRequest", + "fields": { + "foodShoppingLists": { + "type": "List", + "requirement": "Optional", + "adder": "addFoodShoppingList(FoodShoppingList)", + "getter": "getFoodShoppingLists()", + "adderAll": "addFoodShoppingLists(List)" + } + } + }, + "PublishReorderClusterRequest": { + "package": "com.google.android.engage.food.service.PublishReorderClusterRequest", + "fields": { + "reorderCluster": { + "requirement": "Required", + "setter": "setReorderCluster(FoodReorderCluster)", + "type": "FoodReorderCluster", + "getter": "getReorderCluster()" + } + } + }, + "PublishContinueSearchClusterRequest": { + "package": "com.google.android.engage.travel.service.PublishContinueSearchClusterRequest", + "fields": { + "continueSearchCluster": { + "type": "ContinueSearchCluster", + "requirement": "Required", + "setter": "setContinueSearchCluster(ContinueSearchCluster)", + "getter": "getContinueSearchCluster()" + } + } + }, + "PublishReservationClusterRequest": { + "package": "com.google.android.engage.travel.service.PublishReservationClusterRequest", + "fields": { + "reservationCluster": { + "type": "ReservationCluster", + "requirement": "Required", + "setter": "setReservationCluster(ReservationCluster)", + "getter": "getReservationCluster()" + } + } + } + } \ No newline at end of file diff --git a/profilers/perfetto-sql/SKILL.md b/profilers/perfetto-sql/SKILL.md index 3d0cff4..b6eb9d1 100644 --- a/profilers/perfetto-sql/SKILL.md +++ b/profilers/perfetto-sql/SKILL.md @@ -6,7 +6,7 @@ description: Translates natural language data intents into syntactically valid P license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-13' + last-updated: '2026-05-14' keywords: - Android - Perfetto SQL @@ -45,17 +45,16 @@ metadata: > **Precedence Rule:** Always prefer using `SPAN_JOIN` or standard library > functions (for example, `intervals.overlap`) to calculate overlaps > **between two different sets of intervals** . Avoid manual arithmetic if a - > standard library feature or `SPAN_JOIN` can achieve the same result. - > Use the following logic if no built-in alternative exists. + > standard library feature or `SPAN_JOIN` can achieve the same result. Use + > the following logic if no built-in alternative exists. 1. **Condition:** The intervals overlap if `start1 < end2` and `start2 < end1`. 2. **Duration:** The overlap duration is calculated as `MIN(end1, end2) - MAX(start1, start2)` - > **Important:** Incomplete Perfetto slices have a duration - > of -1 (`dur = -1`). Always calculate the effective end time using - > `ts + IIF(dur = -1, trace_end() - ts, dur)` before applying - > this logic. + > **Important:** Incomplete Perfetto slices have a duration of -1 + > (`dur = -1`). Always calculate the effective end time using `ts + + > IIF(dur = -1, trace_end() - ts, dur)` before applying this logic. - Query `android_thread_slices_for_all_startups` for app startup requests. - Join `counter_track` with `counter` to get values of counter with a specific @@ -73,18 +72,15 @@ metadata: - To calculate the total time spent in slices matching a specific name pattern (for example, `*{name_pattern}*`), you must sum their durations. **Why it's useful** : This helps quantify the total impact of a specific function or - feature on performance across multiple calls. Here is an example query - (note the safe handling of incomplete slices): - `sql - SELECT - count(*) as total_count, - sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / 1000000.0 as total_dur_ms - FROM slice - WHERE slice.name GLOB '*{name_pattern}*';` + feature on performance across multiple calls. Here is an example query (note + the safe handling of incomplete slices): `sql SELECT count(*) as + total_count, sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / + 1000000.0 as total_dur_ms FROM slice WHERE slice.name GLOB + '*{name_pattern}*';` ## Resources -- **Documentation:** The Perfetto Standard Library documentation is in [`references/perfetto-stdlib-docs.md`](references/perfetto-stdlib-docs.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib-docs.md`](references/android/agents/skills/profilers/common/perfetto-stdlib.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. - **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. ## Execution Protocol @@ -97,29 +93,30 @@ You must follow these steps sequentially, mirroring a multi-agent pipeline: working directory or in your path, download it directly from the Perfetto index: `curl -LO https://get.perfetto.dev/trace_processor && chmod +x ./trace_processor` -> **Important:** The file served at this URL is a `~10KB` Python -> wrapper script. Don't assume the download failed because it is human-readable -> text. This is the intended behavior. This script handles lazy-loading the real -> binary into `~/.local/share/perfetto/` on its first run. Use it directly. +> **Important:** The file served at this URL is a `~10KB` Python wrapper script. +> Don't assume the download failed because it is human-readable text. This is +> the intended behavior. This script handles lazy-loading the real binary into +> `~/.local/share/perfetto/` on its first run. Use it directly. ### Step 1: Dissection and Schema Research 1. Identify the core question, required data points, and filtering conditions. 2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. -3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](references/perfetto-stdlib-docs.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](references/android/agents/skills/profilers/common/perfetto-stdlib.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. - **Targeted Bounded Reads:** Once you identify the relevant modules, efficiently read the tables and views within that module section. - - **Extract:** Extract only the schema, columns, and the exact `INCLUDE PERFETTO MODULE` statements for the required object from the documentation. + - **Extract:** Extract only the schema, columns, and the exact `INCLUDE + PERFETTO MODULE` statements for the required object from the documentation. - **Verify:** Review the columns, types, and descriptions to ensure the table matches your needs. 4. Print the research results before drafting the query: 5. *Tables/Views:* `Schema for {name}:` listing columns and types. ### Step 2: Draft and Validate Loop (Max 3 Iterations) -Draft the SQL query in SQLite syntax using **only** the schemas -retrieved in Step 1. After drafting, you must validate against this checklist: +Draft the SQL query in SQLite syntax using **only** the schemas retrieved in +Step 1. After drafting, you must validate against this checklist: - \[ \] **SQLite Syntax:** Does the query parse successfully without syntax errors? - \[ \] **Idempotency:** Are all object creations safe to re-run? (Did you use `CREATE OR REPLACE PERFETTO` and `DROP TABLE IF EXISTS` for virtual tables?) diff --git a/profilers/perfetto-sql/references/perfetto-stdlib-docs.md b/profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md similarity index 99% rename from profilers/perfetto-sql/references/perfetto-stdlib-docs.md rename to profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md index 1740395..c1b871b 100644 --- a/profilers/perfetto-sql/references/perfetto-stdlib-docs.md +++ b/profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md @@ -15,17 +15,16 @@ modules in the standard library convert low-level trace concepts e.g. slices, tracks and into concepts developers may be more familar with e.g. for Android developers: app startups, binder transactions etc. -Standard library modules can be included as follows: -\`\`\` --- Include all tables/views/functions from the android.startup.startups --- module in the standard library. -INCLUDE PERFETTO MODULE android.startup.startups; +Standard library modules can be included as follows --- Use the android_startups table defined in the android.startup.startups --- module. -SELECT \* -FROM android_startups; -\`\`\` + -- Include all tables/views/functions from the android.startup.startups + -- module in the standard library. + INCLUDE PERFETTO MODULE android.startup.startups; + + -- Use the android_startups table defined in the android.startup.startups + -- module. + SELECT * + FROM android_startups; Prelude is a special module is automatically included. It contains key helper tables, views and functions which are universally useful. diff --git a/profilers/perfetto-trace-analysis/SKILL.md b/profilers/perfetto-trace-analysis/SKILL.md index 6575e25..9e50747 100644 --- a/profilers/perfetto-trace-analysis/SKILL.md +++ b/profilers/perfetto-trace-analysis/SKILL.md @@ -1,30 +1,27 @@ --- name: perfetto-trace-analysis -description: Analyzes Perfetto traces to find the root cause of latency, memory, +description: Analyzes Perfetto traces to find the root cause of latency, memory, or + jank issues in Android apps. Use when the user provides a Perfetto trace file and + asks any question, ongoing investigation, or open-ended request to analyze its contents. license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-13' + last-updated: '2026-05-14' keywords: - Perfetto - trace analysis - Android performance - - jank - - profiling - debugging - - Perfetto SQL - - performance analysis - - troubleshooting + - profiling + - jank + - bottleneck + - SQL --- -or jank issues in Android apps. Use when the user provides a Perfetto trace file -and asks any question, ongoing investigation, or open-ended request to analyze -its contents. - ## Resources -- **Domain Hints:** Reference files for specific performance areas are located in [`references/hints_[domain].md`](https://developer.android.com/agents/skills/perfetto-trace-analysis/references/hints_*) (e.g., `hints_cpu.md`, `hints_io.md`). To avoid context bloat, do not load all hints at once; read the specific file only when you have narrowed your investigation to that domain. -- **Perfetto SQL Skill:** This workspace may contain a `perfetto-sql` skill that guides translating intents into valid queries. You must search for and locate it, then follow its [Execution Protocol](references/android/agents/skills/profilers/perfetto-sql/skill.md) for all SQL generation. If it is not present, proceed as without it, downloading the trace processor and creating + running the queries to the best of your abilities. +- **Domain Hints:** Reference files for specific performance areas: [`CPU`](references/hints_cpu.md), [`Graphics`](references/hints_graphics.md), [`I/O`](references/hints_io.md), [`IPC`](references/hints_ipc.md), [`Memory`](references/hints_memory.md), [`Power`](references/hints_power.md). +- **Perfetto SQL Reference:** Reference guidelines for translating intents into valid queries are located in [the SQL reference](references/sql.md). You must read this reference and follow its Execution Protocol for all SQL generation. ## Setup Phase (Mandatory) @@ -33,7 +30,7 @@ its contents. - Name the file using the trace's filename appended with `_analysis.md` (e.g., `[trace_filename]_analysis.md`). Before creating it, check if a file with that name already exists by listing the directory's contents---to avoid biasing your investigation, DO NOT read the file's contents to check for its existence. If it does, append an incrementing version number (e.g., `_v2.md`, `_v3.md`) until you find an available filename. You MUST hardcode this exact filename in all subsequent tool calls. - Use this scratchpad STRICTLY to log verified facts: timestamps, slice names, thread IDs (utid/tid), and thread states. - DO NOT write preliminary hypotheses or premature conclusions in the scratchpad. It is a strict Chain of Evidence. -2. **Locate SQL Skill:** Search your workspace for the `perfetto-sql` skill (e.g., using `glob` or `list_directory`). If available, you MUST locate and follow its [Execution Protocol](references/android/agents/skills/profilers/perfetto-sql/skill.md) for all SQL generation. If it is not present, proceed as normal. Do not guess schemas. +2. **Review SQL Reference:** Read the SQL reference in [`references/sql.md`](references/sql.md) and follow its Execution Protocol for all SQL generation. Do not guess schemas. 3. **Target Resolution:** If the user's request is broad (e.g., "why is the app slow?") and doesn't specify a package name: - Execute a query to identify the active application: `sql INCLUDE PERFETTO MODULE android.startup.startups; SELECT package FROM @@ -46,7 +43,7 @@ Follow this iterative loop until you have isolated the definitive root cause(s): ### 1. Formulate Hypothesis -- **Prioritization:** Form hypotheses using information from: user prompt \> built-in hints ([`references/hints_*.md`](https://developer.android.com/agents/skills/perfetto-trace-analysis/references/hints_*)) \> general knowledge. +- **Prioritization:** Form hypotheses using information from: user prompt \> built-in hints ([`references/hints_*.md`](references/hints_cpu.md)) \> general knowledge. - **Source Attribution:** Explicitly mention the source of your hypothesis (e.g., "Based on hints_io.md..."). - **Focus Constraint:** Focus on the primary bottleneck. Avoid investigating deep into binder transactions unless the user explicitly asks for it or there is no other obvious bottleneck. - **State Reasoning:** Briefly state your reasoning based on previous findings *before* generating a new query. diff --git a/profilers/perfetto-trace-analysis/references/hints_cpu.md b/profilers/perfetto-trace-analysis/references/hints_cpu.md new file mode 100644 index 0000000..f03e492 --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_cpu.md @@ -0,0 +1,16 @@ +- When debugging a long slice: Examine its thread states to understand what the thread was doing (running, sleeping, blocked on I/O). +- When debugging a long slice for latency issues: Check if its duration is caused by one or more long-running child slices. Apply recursively. +- When a thread is woken up but there's a delay before it runs, check the "IRQ" track for the corresponding CPU to see if an interrupt is the cause. +- Check if kernel threads associated with hardware are running with real-time priority; if not, they can be preempted. +- When analysis of the primary application package does not reveal the root cause, expand to all threads and processes. Search for other runnable threads on the same CPU or high-priority kernel threads. +- When investigating app startup, use an SQL query to aggregate the reasons for uninterruptible sleep on the main thread. +- Check the "cpu_frequency" counter for the CPU cores that ran the main process. Missing frequency data or stuck frequencies indicate a kernel-level bug in the governor. +- Query raw "ftrace" events for logs related to the governor thread (e.g., "su_gov"). +- To find concurrency issues, search for critical threads (e.g., 'RenderThread') in a blocked state (thread_state.state = 'D') and join with scheduling data to find the waker. +- Compare time spent in userspace functions vs kernel (slices with \[k\] prefix). +- To detect a 'catch-up storm', look for threads with long gaps in thread_state/cpu_slice activity immediately followed by a high-density burst. +- Quantify scheduler contention by calculating scheduling latency (measure duration of preceding 'Runnable' state using preceding_sched_slice_for_thread). Search for maximums and high percentiles (p95/p99). +- If a task exhibits high scheduling latency, check if other CPUs were idle (running swapper or idle thread). +- Check the cpu_id for key threads; if consistently scheduled on slower cores, it signals a potential performance gain by allowing them on big cores. +- For a struggling thread, analyze 'Runnable' vs 'Running' state time. A large 'Runnable' time indicates CPU contention. +- If a slice's wall duration increases but the percentage of 'Running' time is unchanged, it strongly suggests a lower CPU frequency. Check sched_switch to focus on the correct cores. \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/hints_graphics.md b/profilers/perfetto-trace-analysis/references/hints_graphics.md new file mode 100644 index 0000000..65d1a85 --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_graphics.md @@ -0,0 +1,11 @@ +- When investigating UI jank, check for long-running slices on the main thread; if a slice like "ConstraintLayout.onMeasure" is taking a long time (e.g., \>8ms), it is a likely cause of the jank. +- When a bitmap_write_to_parcel slice takes milliseconds instead of microseconds, investigate its children slices to see how time is spent. Long durations often point to suboptimal kernel operations like memory mapping (mmap) or unnecessary data zeroing. +- For a high-level overview of graphics memory usage, track the 'gpu_mem_total' counter for a specific process (upid). +- To find the largest graphics allocations, query the android_graphics_allocs table and sort by size_bytes in descending order. Compare its 'width' and 'height' against the device's display resolution. +- To detect the "double memory cost" of an image existing on both CPU and GPU, look for a large buffer in android_graphics_allocs and a simultaneous CPU memory allocation of a similar size (rss_anon_bytes or heap_graph). +- To find potentially costly intermediate render targets, look for large buffers in android_graphics_allocs where 'usage_bits' lack a 'COMPOSER_OVERLAY' flag. +- To check if an allocation is actually presented on screen, correlate 'buffer_id' with SurfaceFlinger events. +- To see if high graphics memory is causing performance issues, check the frame duration in 'actual_frame_timeline_frame'. Durations over vsync (e.g., 16.6ms) correlating with 'gpu_mem_total' spikes suggest memory pressure jank. +- Within the main thread of a janky graphics application, look for frequent or long-running 'texture_upload' slices. +- Analyze the duration of 'eglSwapBuffersWithDamageKHR' or similar buffer-swapping slices in the graphics rendering thread. Consistently long durations suggest a large "damage area" being redrawn every frame. +- To identify UI jank, compare the 'actual_frame_timeline' against the 'expected_frame_timeline' on the main process; a significant deviation indicates missed frames. \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/hints_io.md b/profilers/perfetto-trace-analysis/references/hints_io.md new file mode 100644 index 0000000..ef2f66d --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_io.md @@ -0,0 +1,12 @@ +- When debugging a long, uninterruptible sleep state related to I/O: Check for overlapping slices with "verity" in their name (dm-verity). +- When a thread is stuck in an uninterruptible sleep with no blocked_function, look for other threads that might be holding the memory lock (e.g., "jit-thread-pool", memory mapping ops). +- When analyzing a long uninterruptible sleep, check the "blocked_function" in the thread state details (from sched_blocked_reason ftrace event). +- A lot of time spent in "do_page_fault" during app startup is a strong indicator of I/O contention. +- For file integrity mechanisms like DM-Verity, search for events like dm_verity_fec_prefetch. +- To find the specific kernel dependency of a stalled app thread, locate the thread in state 'D', then look for kworker or kernel threads that become runnable immediately after. +- For app stalls caused by I/O, analyze the scheduling latency of the relevant kworker threads handling the request. +- To find inefficient file I/O, query the syscall table for a high frequency of small, sequential read() or pread() syscalls on a single fd. +- If a thread spends significant time in 'Uninterruptible Sleep', check if 'blocked_function' is 'page_cache_readahead'. Correlate waking timestamps with 'filemap_add_to_page_cache' ftrace events. +- Aggregate counts of 'filemap_add_to_page_cache' grouping by 'inode' to find the specific file causing I/O pressure. +- Inspect 'nr_sector' in 'block_rq_issue' ftrace events to understand file read-ahead size. +- If an I/O issue disappears on subsequent launches, it's a 'cold start' problem (populating page cache). \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/hints_ipc.md b/profilers/perfetto-trace-analysis/references/hints_ipc.md new file mode 100644 index 0000000..4ae569e --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_ipc.md @@ -0,0 +1,10 @@ +- Look for multiple outbound binder transactions from the same process (system_server) that carry similar data to different destinations in a short time frame. This "binder storm" indicates a lack of multiplexing. +- To trace data across processes, correlate slices using flow events by linking a slice's ID to flow.source_slice_id or flow.dest_slice_id. +- To detect binder spam, query the binder_transaction table and group by thread ID (tid), service_name, and method_name to find high numbers of identical calls. +- When high binder concurrency is found, identify the bottleneck server process by grouping transactions by server_upid. +- To analyze the latency of a slow binder transaction, calculate the time spent outside the server by subtracting server_dur from the total dur in the binder_transaction table. +- When a thread is suspected of binder spam, correlate its tid with the cpu_slice table to check for high CPU consumption. +- To find the code responsible for binder spam, get the utid of the problematic thread and use it to query stack_profile_callsite. +- To find the callers of a problematic function, filter stack_profile_callsite for frames mapping to it, then trace upwards using parent_id. +- A long-running slice on one thread causally linked to a slice on another thread (e.g., binder from system_server to SystemUI) indicates a scheduling dependency bottleneck. +- To find asynchronous operations that might cause UI jank, look for a binder transaction from a controlling process that returns quickly, followed by a long-running slice in the receiving process. \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/hints_memory.md b/profilers/perfetto-trace-analysis/references/hints_memory.md new file mode 100644 index 0000000..eba3a90 --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_memory.md @@ -0,0 +1,15 @@ +- To investigate low memory kills, look for the "lmk_kill_occurred" ftrace event; a high count indicates severe memory pressure. +- When you see a burst of "lmk_kill_occurred" events, query for processes with high CPU wall_duration in the "sched_slice" table during the same time window. Runaway processes consuming CPU exacerbate memory pressure. +- To understand a process's memory impact, inspect its "anon_rss" (Anonymous Resident Set Size) from memory counters like "mem.info". +- Check system-wide memory stats for a high or rapidly increasing "swap_used" value. +- To confirm memory thrashing, look for high CPU usage by the "kswapd" kernel thread. +- To find the direct trigger for LMKD, look for "memory_pressure" trace events from Pressure Stall Information (PSI). +- To detect 'lost' memory for processes using hardware accelerators (like a TPU), query the counter table for RssFile values. If it drops significantly while hardware is active, check for kswapd0 scheduling slices. +- When analyzing memory shared with hardware, query the dma_heap_stat and dmabuf_total_size counters (or ion_total_size for older devices) for a more accurate picture than RSS. +- Be aware that RssFile can over-report memory if the same physical page is mapped multiple times. +- When investigating OOMs, establish a baseline by querying process memory counters (e.g., mem.rss) and compare the median and 95th percentile against its history. +- If bitmaps are a major memory consumer, check for outliers in bitmap count by querying android.graphics.Bitmap instances across traces. +- To find what is holding onto an object (like a bitmap), trace its retainer path back to a GC root by querying heap_graph_reference. Pay close attention to custom application classes. +- Query the android_bitmaps table to check the width and height properties of bitmaps. +- Check for the presence of software bitmaps, which consume memory on both the app heap and in graphics memory. +- When analyzing bitmaps, look for duplicates by checking for multiple android.graphics.Bitmap objects with identical properties. \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/hints_power.md b/profilers/perfetto-trace-analysis/references/hints_power.md new file mode 100644 index 0000000..ac2e9ad --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/hints_power.md @@ -0,0 +1,7 @@ +- When investigating overall battery drain, start by querying the power_rails track and summing the energy consumed (power_ma \* duration) for each rail name. +- To check if a device is sleeping correctly during screen-off periods, query the suspend_state track; a lack of time in "suspended" state indicates a wakefulness problem. +- To find the root cause of the device failing to suspend, query the kernel_wakelock track and aggregate the total duration for each wake lock name. +- If a top kernel wake lock name contains "bt_" or "bcm" (e.g., bt_host_wake), cross-reference its timing with events in the bluetooth_scan_results track. +- If the modem rail in power_rails shows high consumption, query the network_packets table and aggregate traffic volume by uid to identify the app. +- When analyzing network traffic from a shared uid, use the socket_tag associated with network packets for granular attribution. +- Convert impact into a common energy unit like milliwatt-hours (mWh) to compare the severity of different issues. \ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md b/profilers/perfetto-trace-analysis/references/sql.md similarity index 84% rename from profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md rename to profilers/perfetto-trace-analysis/references/sql.md index 7ab3311..8ec1baa 100644 --- a/profilers/perfetto-trace-analysis/references/android/agents/skills/profilers/perfetto-sql/skill.md +++ b/profilers/perfetto-trace-analysis/references/sql.md @@ -25,17 +25,16 @@ > **Precedence Rule:** Always prefer using `SPAN_JOIN` or standard library > functions (for example, `intervals.overlap`) to calculate overlaps > **between two different sets of intervals** . Avoid manual arithmetic if a - > standard library feature or `SPAN_JOIN` can achieve the same result. - > Use the following logic if no built-in alternative exists. + > standard library feature or `SPAN_JOIN` can achieve the same result. Use + > the following logic if no built-in alternative exists. 1. **Condition:** The intervals overlap if `start1 < end2` and `start2 < end1`. 2. **Duration:** The overlap duration is calculated as `MIN(end1, end2) - MAX(start1, start2)` - > **Important:** Incomplete Perfetto slices have a duration - > of -1 (`dur = -1`). Always calculate the effective end time using - > `ts + IIF(dur = -1, trace_end() - ts, dur)` before applying - > this logic. + > **Important:** Incomplete Perfetto slices have a duration of -1 + > (`dur = -1`). Always calculate the effective end time using `ts + + > IIF(dur = -1, trace_end() - ts, dur)` before applying this logic. - Query `android_thread_slices_for_all_startups` for app startup requests. - Join `counter_track` with `counter` to get values of counter with a specific @@ -53,18 +52,15 @@ - To calculate the total time spent in slices matching a specific name pattern (for example, `*{name_pattern}*`), you must sum their durations. **Why it's useful** : This helps quantify the total impact of a specific function or - feature on performance across multiple calls. Here is an example query - (note the safe handling of incomplete slices): - `sql - SELECT - count(*) as total_count, - sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / 1000000.0 as total_dur_ms - FROM slice - WHERE slice.name GLOB '*{name_pattern}*';` + feature on performance across multiple calls. Here is an example query (note + the safe handling of incomplete slices): `sql SELECT count(*) as + total_count, sum(IIF(slice.dur = -1, trace_end() - slice.ts, slice.dur)) / + 1000000.0 as total_dur_ms FROM slice WHERE slice.name GLOB + '*{name_pattern}*';` ## Resources -- **Documentation:** The Perfetto Standard Library documentation is in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/perfetto-sql/references/perfetto-stdlib-docs). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/common/perfetto-stdlib). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. - **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. ## Execution Protocol @@ -77,29 +73,30 @@ You must follow these steps sequentially, mirroring a multi-agent pipeline: working directory or in your path, download it directly from the Perfetto index: `curl -LO https://get.perfetto.dev/trace_processor && chmod +x ./trace_processor` -> **Important:** The file served at this URL is a `~10KB` Python -> wrapper script. Don't assume the download failed because it is human-readable -> text. This is the intended behavior. This script handles lazy-loading the real -> binary into `~/.local/share/perfetto/` on its first run. Use it directly. +> **Important:** The file served at this URL is a `~10KB` Python wrapper script. +> Don't assume the download failed because it is human-readable text. This is +> the intended behavior. This script handles lazy-loading the real binary into +> `~/.local/share/perfetto/` on its first run. Use it directly. ### Step 1: Dissection and Schema Research 1. Identify the core question, required data points, and filtering conditions. 2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. -3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/perfetto-sql/references/perfetto-stdlib-docs). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/common/perfetto-stdlib). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. - **Targeted Bounded Reads:** Once you identify the relevant modules, efficiently read the tables and views within that module section. - - **Extract:** Extract only the schema, columns, and the exact `INCLUDE PERFETTO MODULE` statements for the required object from the documentation. + - **Extract:** Extract only the schema, columns, and the exact `INCLUDE + PERFETTO MODULE` statements for the required object from the documentation. - **Verify:** Review the columns, types, and descriptions to ensure the table matches your needs. 4. Print the research results before drafting the query: 5. *Tables/Views:* `Schema for {name}:` listing columns and types. ### Step 2: Draft and Validate Loop (Max 3 Iterations) -Draft the SQL query in SQLite syntax using **only** the schemas -retrieved in Step 1. After drafting, you must validate against this checklist: +Draft the SQL query in SQLite syntax using **only** the schemas retrieved in +Step 1. After drafting, you must validate against this checklist: - \[ \] **SQLite Syntax:** Does the query parse successfully without syntax errors? - \[ \] **Idempotency:** Are all object creations safe to re-run? (Did you use `CREATE OR REPLACE PERFETTO` and `DROP TABLE IF EXISTS` for virtual tables?) From 3a68d97de22c4287ed237011a2e9d451704e7e3a Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 15 May 2026 14:52:58 +0000 Subject: [PATCH 17/61] Updates skills (2026-05-15 14:52) --- profilers/perfetto-sql/SKILL.md | 4 +- .../references/perfetto-stdlib.md | 6670 +++++++++++++++++ .../references/perfetto-stdlib.md | 6670 +++++++++++++++++ .../perfetto-trace-analysis/references/sql.md | 4 +- 4 files changed, 13344 insertions(+), 4 deletions(-) create mode 100644 profilers/perfetto-sql/references/perfetto-stdlib.md create mode 100644 profilers/perfetto-trace-analysis/references/perfetto-stdlib.md diff --git a/profilers/perfetto-sql/SKILL.md b/profilers/perfetto-sql/SKILL.md index b6eb9d1..20712b0 100644 --- a/profilers/perfetto-sql/SKILL.md +++ b/profilers/perfetto-sql/SKILL.md @@ -80,7 +80,7 @@ metadata: ## Resources -- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib-docs.md`](references/android/agents/skills/profilers/common/perfetto-stdlib.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib.md`](references/perfetto-stdlib.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. - **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. ## Execution Protocol @@ -102,7 +102,7 @@ working directory or in your path, download it directly from the Perfetto index: 1. Identify the core question, required data points, and filtering conditions. 2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. -3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](references/android/agents/skills/profilers/common/perfetto-stdlib.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`perfetto-stdlib.md`](references/perfetto-stdlib.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. diff --git a/profilers/perfetto-sql/references/perfetto-stdlib.md b/profilers/perfetto-sql/references/perfetto-stdlib.md new file mode 100644 index 0000000..a2e9c6c --- /dev/null +++ b/profilers/perfetto-sql/references/perfetto-stdlib.md @@ -0,0 +1,6670 @@ +*This page documents the PerfettoSQL standard library.* + +## Introduction + +The PerfettoSQL standard library is a repository of tables, views, functions +and macros, contributed by domain experts, which make querying traces easier +Its design is heavily inspired by standard libraries in languages like Python, +C++ and Java. + +Some of the purposes of the standard library include: +1) Acting as a way of sharing and commonly written queries without needing +to copy/paste large amounts of SQL. +2) Raising the abstraction level when exposing data in the trace. Many +modules in the standard library convert low-level trace concepts +e.g. slices, tracks and into concepts developers may be more familar with +e.g. for Android developers: app startups, binder transactions etc. + +Standard library modules can be included as follows + + -- Include all tables/views/functions from the android.startup.startups + -- module in the standard library. + INCLUDE PERFETTO MODULE android.startup.startups; + + -- Use the android_startups table defined in the android.startup.startups + -- module. + SELECT * + FROM android_startups; + +Prelude is a special module is automatically included. It contains key helper +tables, views and functions which are universally useful. + +More information on importing modules is available in the +[syntax documentation](https://perfetto.dev/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) +for the `INCLUDE PERFETTO MODULE` statement. + +## Package: prelude + +#### Views/Tables + +
+ +**trace_metrics**. Lists all metrics built-into trace processor. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the metric. | + +
+ +
+ +
+ +**track**. Tracks are a fundamental concept in trace processor and represent a "timeline" for events of the same type and with the same context + +
+ +VIEW +Tracks are a fundamental concept in trace processor and represent a +"timeline" for events of the same type and with the same context. See +https://perfetto.dev/docs/analysis/trace-processor#tracks for a more +detailed explanation, with examples. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this track. Identical to \|track_id\|, prefer using \|track_id\| instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track; can be null for some types of tracks (e.g. thread tracks). | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. Alias of `args.arg_set_id`. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | + +
+ +
+ +
+ +**cpu**. Contains information about the CPUs on the device this trace was taken on. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this CPU. Identical to \|ucpu\|, prefer using \|ucpu\| instead. | +| ucpu | UINT | Unique identifier for this CPU. Isn't equal to \|cpu\| for remote machines and is equal to \|cpu\| for the host machine. | +| cpu | UINT | The 0-based CPU core identifier. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| cluster_id | UINT | The cluster id is shared by CPUs in the same cluster. | +| processor | STRING | A string describing this core. | +| machine_id | UINT | Machine identifier, non-null for CPUs on a remote machine. | +| capacity | UINT | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt | +| arg_set_id | UINT | Extra key/value pairs associated with this cpu. | + +
+ +
+ +
+ +**cpu_available_frequencies**. Contains the frequency values that the CPUs on the device are capable of running at. + +
+ +VIEW +Contains the frequency values that the CPUs on the device are capable of +running at. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu frequency. | +| cpu | UINT | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| freq | UINT | CPU frequency in KHz. | +| ucpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | + +
+ +
+ +
+ +**sched_slice**. This table holds slices with kernel thread scheduling information + +
+ +VIEW +This table holds slices with kernel thread scheduling information. These +slices are collected when the Linux "ftrace" data source is used with the +"sched/switch" and "sched/wakeup\*" events enabled. + +The rows in this table will always have a matching row in the \|thread_state\| +table with \|thread_state.state\| = 'Running' + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | + +
+ +
+ +
+ +**sched** . Shorter alias for table `sched_slice`. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Alias for `sched_slice.id`. | +| type | STRING | Alias for `sched_slice.type`. | +| ts | LONG | Alias for `sched_slice.ts`. | +| dur | LONG | Alias for `sched_slice.dur`. | +| cpu | UINT | Alias for `sched_slice.cpu`. | +| utid | UINT | Alias for `sched_slice.utid`. | +| end_state | STRING | Alias for `sched_slice.end_state`. | +| priority | INT | Alias for `sched_slice.priority`. | +| ucpu | UINT | Alias for `sched_slice.ucpu`. | +| ts_end | UINT | Legacy column, should no longer be used. | + +
+ +
+ +
+ +**thread_state**. This table contains the scheduling state of every thread on the system during the trace. The rows in this table which have \|state\| = 'Running', will have a corresponding row in the \|sched_slice\| table. + +
+ +VIEW +This table contains the scheduling state of every thread on the system during +the trace. + +The rows in this table which have \|state\| = 'Running', will have a +corresponding row in the \|sched_slice\| table. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this thread state. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in \|sched_slice.end_state\|. | +| io_wait | UINT | Indicates whether this thread was blocked on IO. | +| blocked_function | STRING | The function in the kernel this thread was blocked on. | +| waker_utid | UINT | The unique thread id of the thread which caused a wakeup of this thread. | +| waker_id | UINT | The unique thread state id which caused a wakeup of this thread. | +| irq_context | UINT | Whether the wakeup was from interrupt context or process context. | +| ucpu | UINT | The unique CPU identifier that the thread executed on. | + +
+ +
+ +
+ +**raw**. Contains 'raw' events from the trace for some types of events + +
+ +VIEW +Contains 'raw' events from the trace for some types of events. This table +only exists for debugging purposes and should not be relied on in production +usecases (i.e. metrics, standard library etc.) + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this raw event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | + +
+ +
+ +
+ +**ftrace_event**. Contains all the ftrace events in the trace + +
+ +VIEW +Contains all the ftrace events in the trace. This table exists only for +debugging purposes and should not be relied on in production usecases (i.e. +metrics, standard library etc). Note also that this table might be empty if +raw ftrace parsing has been disabled. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this ftrace event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | + +
+ +
+ +
+ +**experimental_sched_upid**. The sched_slice table with the upid column. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | +| upid | UINT | The process's unique id in the trace. | + +
+ +
+ +
+ +**cpu_track**. Tracks which are associated to a single CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**cpu_counter_track**. Tracks containing counter-like events associated to a CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu counter track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**counters** . Alias of the `counter` table. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias of `counter.id`. | +| type | STRING | Alias of `counter.type`. | +| ts | LONG | Alias of `counter.ts`. | +| track_id | INT | Alias of `counter.track_id`. | +| value | DOUBLE | Alias of `counter.value`. | +| arg_set_id | INT | Alias of `counter.arg_set_id`. | +| name | STRING | Legacy column, should no longer be used. | +| unit | STRING | Legacy column, should no longer be used. | +| description | STRING | Legacy column, should no longer be used. | + +
+ +
+ +
+ +**slice**. Contains slices from userspace which explains what threads were doing during the trace. + +
+ +VIEW +Contains slices from userspace which explains what threads were doing +during the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | LONG | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | LONG | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_ts | LONG | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| cat | STRING | Alias of `category`. | +| slice_id | LONG | Alias of `id`. | + +
+ +
+ +
+ +**instant**. Contains instant events from userspace which indicates what happened at a single moment in time. + +
+ +VIEW +Contains instant events from userspace which indicates what happened at a +single moment in time. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | The timestamp of the instant (in nanoseconds). | +| track_id | INT | The id of the track this instant is located on. | +| name | STRING | The name of the instant. The name describes what happened during the instant. | +| arg_set_id | INT | The id of the argument set associated with this instant. | + +
+ +
+ +
+ +**slices** . Alternative alias of table `slice`. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Alias of `slice.id`. | +| type | STRING | Alias of `slice.type`. | +| ts | LONG | Alias of `slice.ts`. | +| dur | LONG | Alias of `slice.dur`. | +| track_id | INT | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | INT | Alias of `slice.depth`. | +| stack_id | LONG | Alias of `slice.stack_id`. | +| parent_stack_id | LONG | Alias of `slice.parent_stack_id`. | +| parent_id | INT | Alias of `slice.parent_id`. | +| arg_set_id | INT | Alias of `slice.arg_set_id`. | +| thread_ts | LONG | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | +| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | +| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | +| cat | LONG | Alias of `slice.cat`. | +| slice_id | LONG | Alias of `slice.slice_id`. | + +
+ +
+ +
+ +**thread**. Contains information of threads seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the thread. Prefer using `utid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| utid | INT | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| tid | INT | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|utid\| instead. | +| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| upid | LONG | The process hosting this thread. | +| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | +| machine_id | INT | Machine identifier, non-null for threads on a remote machine. | + +
+ +
+ +
+ +**process**. Contains information of processes seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the process. Prefer using `upid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| upid | LONG | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|upid\| instead. | +| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| parent_upid | INT | The upid of the process which caused this process to be spawned. | +| uid | INT | The Unix user id of the process. | +| android_appid | INT | Android appid of this process. | +| cmdline | STRING | /proc/cmdline for this process. | +| arg_set_id | INT | Extra args for this process. | +| machine_id | INT | Machine identifier, non-null for processes on a remote machine. | + +
+ +
+ +
+ +**args**. Arbitrary key-value pairs which allow adding metadata to other, strongly typed tables. Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| will be non-null. + +
+ +VIEW +Arbitrary key-value pairs which allow adding metadata to other, strongly +typed tables. +Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| +will be non-null. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the arg. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| arg_set_id | INT | The id for a single set of arguments. | +| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | +| key | STRING | The key for the arg. | +| int_value | LONG | The integer value of the arg. | +| string_value | STRING | The string value of the arg. | +| real_value | DOUBLE | The double value of the arg. | +| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | +| display_value | STRING | The human-readable formatted value of the arg. | + +
+ +
+ +
+ +**perf_session**. Contains the Linux perf sessions in the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the perf session. Prefer using `perf_session_id` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| perf_session_id | INT | The id of the perf session. | +| cmdline | STRING | Command line used to collect the data. | + +
+ +
+ +#### Functions + +
+ +**slice_is_ancestor** -\> BOOL. Given two slice ids, returns whether the first is an ancestor of the second. + +
+ +
+ +
+ +Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. + +| Argument | Type | Description | +|---|---|---| +| ancestor_id | LONG | Id of the potential ancestor slice. | +| descendant_id | LONG | Id of the potential descendant slice. | + +
+ +
+ +
+ +**trace_start** -\> LONG. Fetch start of the trace. + +
+ +
+ +
+ +Returns LONG: Start of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_end** -\> LONG. Fetch end of the trace. + +
+ +
+ +
+ +Returns LONG: End of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_dur** -\> LONG. Fetch duration of the trace. + +
+ +
+ +
+ +Returns LONG: Duration of the trace in nanoseconds. + +
+ +
+ +#### Macros + +
+ +**cast_int** . Casts \|value\| to INT. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +
+ +**cast_double** . Casts \|value\| to DOUBLE. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +
+ +**cast_string** . Casts \|value\| to STRING. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +## Package: v8 + +### v8.jit + +#### Views/Tables + +
+ +**v8_isolate**. A V8 Isolate instance + +
+ +VIEW +A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8 +engine. + +| Column | Type | Description | +|---|---|---| +| v8_isolate_id | UINT | Unique V8 isolate id. | +| upid | UINT | Process the isolate was created in. | +| internal_isolate_id | UINT | Internal id used by the v8 engine. Unique in a process. | +| embedded_blob_code_start_address | LONG | Absolute start address of the embedded code blob. | +| embedded_blob_code_size | LONG | Size in bytes of the embedded code blob. | +| code_range_base_address | LONG | Base address of the code range if the isolate defines one. | +| code_range_size | LONG | Size of a code range if the isolate defines one. | +| shared_code_range | LONG | Whether the code range for this Isolate is shared with others in the same process. There is at max one such shared code range per process. | +| embedded_blob_code_copy_start_address | LONG | Used when short builtin calls are enabled, where embedded builtins are copied into the CodeRange so calls can be nearer. | + +
+ +
+ +
+ +**v8_js_script**. Represents a script that was compiled to generate code + +
+ +VIEW +Represents a script that was compiled to generate code. Some V8 code is +generated out of scripts and will reference a V8Script other types of code +will not (e.g. builtins). + +| Column | Type | Description | +|---|---|---| +| v8_js_script_id | UINT | Unique V8 JS script id. | +| v8_isolate_id | UINT | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | +| script_type | STRING | Script type. | +| name | STRING | Script name. | +| source | STRING | Actual contents of the script. | + +
+ +
+ +
+ +**v8_wasm_script**. Represents one WASM script. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| v8_wasm_script_id | UINT | Unique V8 WASM script id. | +| v8_isolate_id | UINT | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | +| url | STRING | URL of the source. | +| source | STRING | Actual contents of the script. | + +
+ +
+ +
+ +**v8_js_function**. Represents a v8 Javascript function. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| v8_js_function_id | UINT | Unique V8 JS function id. | +| name | STRING | Function name. | +| v8_js_script_id | UINT | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | +| is_toplevel | BOOL | Whether this function represents the top level script. | +| kind | STRING | Function kind (e.g. regular function or constructor). | +| line | UINT | Line in script where function is defined. Starts at 1. | +| col | UINT | Column in script where function is defined. Starts at 1. | + +
+ +
+ +## Package: wattson + +### wattson.system_state + +#### Views/Tables + +
+ +**wattson_system_states**. The final system state for the CPU subsystem, which has all the information needed by Wattson to estimate energy for the CPU subsystem. + +
+ +TABLE +The final system state for the CPU subsystem, which has all the information +needed by Wattson to estimate energy for the CPU subsystem. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Starting timestamp of the current counter where system state is constant. | +| dur | INT | Duration of the current counter where system state is constant. | +| l3_hit_count | INT | Number of L3 hits the current system state. | +| l3_miss_count | INT | Number of L3 misses in the current system state. | +| freq_0 | INT | Frequency of CPU0. | +| idle_0 | INT | Idle state of CPU0. | +| freq_1 | INT | Frequency of CPU1. | +| idle_1 | INT | Idle state of CPU1. | +| freq_2 | INT | Frequency of CPU2. | +| idle_2 | INT | Idle state of CPU2. | +| freq_3 | INT | Frequency of CPU3. | +| idle_3 | INT | Idle state of CPU3. | +| freq_4 | INT | Frequency of CPU4. | +| idle_4 | INT | Idle state of CPU4. | +| freq_5 | INT | Frequency of CPU5. | +| idle_5 | INT | Idle state of CPU5. | +| freq_6 | INT | Frequency of CPU6. | +| idle_6 | INT | Idle state of CPU6. | +| freq_7 | INT | Frequency of CPU7. | +| idle_7 | INT | Idle state of CPU7. | +| suspended | BOOL | Flag indicating if current system state is suspended. | + +
+ +
+ +## Package: stacks + +### stacks.cpu_profiling + +#### Views/Tables + +
+ +**cpu_profiling_samples**. Table containing all the timestamped samples of CPU profiling which occurred during the trace. Currently, this table is backed by the following data sources: \* Linux perf \* macOS instruments \* Chrome CPU profiling \* Legacy V8 CPU profiling \* Profiling data in Gecko traces + +
+ +TABLE +Table containing all the timestamped samples of CPU profiling which occurred +during the trace. + +Currently, this table is backed by the following data sources: +\* Linux perf +\* macOS instruments +\* Chrome CPU profiling +\* Legacy V8 CPU profiling +\* Profiling data in Gecko traces + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the sample. | +| ts | INT | The timestamp of the sample. | +| utid | INT | The utid of the thread of the sample, if available. | +| tid | INT | The tid of the sample, if available. | +| thread_name | STRING | The thread name of thread of the sample, if available. | +| ucpu | INT | The ucpu of the sample, if available. | +| cpu | INT | The cpu of the sample, if available. | +| callsite_id | INT | The callsite id of the sample. | + +
+ +
+ +
+ +**cpu_profiling_summary_tree** . Table summarising the callstacks captured during any CPU profiling which occurred during the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. The data sources supported are the same as the `cpu_profiling_samples` table. + +
+ +TABLE +Table summarising the callstacks captured during any CPU profiling which +occurred during the trace. + +Specifically, this table returns a tree containing all the callstacks seen +during the trace with `self_count` equal to the number of samples with that +frame as the leaf and `cumulative_count` equal to the number of samples with +the frame anywhere in the tree. + +The data sources supported are the same as the `cpu_profiling_samples` table. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | +| parent_id | INT | The id of the parent callstack for this callstack. NULL if this is root. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
+ +## Package: pkvm + +### pkvm.hypervisor + +#### Views/Tables + +
+ +**pkvm_hypervisor_events**. Events when CPU entered hypervisor. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the corresponding slice in slices table. | +| cpu | INT | CPU that entered hypervisor. | +| ts | INT | Timestamp when CPU entered hypervisor (in nanoseconds). | +| dur | INT | How much time CPU spent in hypervisor (in nanoseconds). | +| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | + +
+ +
+ +## Package: slices + +### slices.cpu_time + +#### Views/Tables + +
+ +**thread_slice_cpu_time**. Time each thread slice spent running on CPU. Requires scheduling data to be available in the trace. + +
+ +TABLE +Time each thread slice spent running on CPU. +Requires scheduling data to be available in the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| cpu_time | INT | Duration of the time the slice was running. | + +
+ +
+ +
+ +**thread_slice_cpu_cycles**. CPU cycles per each slice. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +### slices.with_context + +#### Views/Tables + +
+ +**thread_slice**. All thread slices with data about thread, thread track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All thread slices with data about thread, thread track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `thread_track.name`. | +| thread_name | STRING | Alias for `thread.name`. | +| utid | INT | Alias for `thread.utid`. | +| tid | INT | Alias for `thread.tid`. | +| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +
+ +**process_slice**. All process slices with data about process track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All process slices with data about process track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `process_track.name`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +## Package: sched + +### sched.runnable + +#### Views/Tables + +
+ +**sched_previous_runnable_on_thread**. Previous runnable slice on the same thread. For each "Running" thread state finds: - previous "Runnable" (or runnable preempted) state. - previous uninterrupted "Runnable" state with a valid waker thread. + +
+ +TABLE +Previous runnable slice on the same thread. +For each "Running" thread state finds: +- previous "Runnable" (or runnable preempted) state. +- previous uninterrupted "Runnable" state with a valid waker thread. + +| Column | Type | Description | +|---|---|---| +| id | INT | `thread_state.id` id. | +| prev_runnable_id | INT | Previous runnable `thread_state.id`. | +| prev_wakeup_runnable_id | INT | Previous runnable `thread_state.id` with valid waker thread. | + +
+ +
+ +### sched.states + +#### Functions + +
+ +**sched_state_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state to a human-readable string. + +
+ +
+ +
+ +Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). + +| Argument | Type | Description | +|---|---|---| +| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | + +
+ +
+ +
+ +**sched_state_io_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state and IO wait information to a human-readable string. + +
+ +
+ +
+ +Translates a single-letter scheduling state and IO wait information to +a human-readable string. +Returns STRING: A human readable string with information about the scheduling state and IO wait. + +| Argument | Type | Description | +|---|---|---| +| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | + +
+ +
+ +### sched.thread_level_parallelism + +#### Views/Tables + +
+ +**sched_runnable_thread_count**. The count of runnable threads over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the runnable thread count changed to the current value. | +| runnable_thread_count | INT | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +
+ +**sched_active_cpu_count**. The count of active CPUs over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the number of active CPU changed. | +| active_cpu_count | INT | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +### sched.time_in_state + +#### Views/Tables + +
+ +**sched_time_in_state_for_thread**. The time a thread spent in each scheduling state during it's lifetime. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| total_runtime | INT | Total runtime of thread. | +| state | STRING | One of the scheduling states of kernel thread. | +| time_in_state | INT | Total time spent in the scheduling state. | +| percentage_in_state | INT | Percentage of time thread spent in scheduling state in \[0-100\] range. | + +
+ +
+ +
+ +**sched_percentage_of_time_in_state**. Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] ranges) + +
+ +TABLE +Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] +ranges). Sum of all states might be smaller than 100, as those values +are rounded down. + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| running | INT | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | +| runnable | INT | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | +| runnable_preempted | INT | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | +| sleeping | INT | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | +| uninterruptible_sleep | INT | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | +| other | INT | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | + +
+ +
+ +#### Table Functions + +
+ +**sched_time_in_state_for_thread_in_interval** . Time the thread spent each state in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_and_cpu_for_thread_in_interval** . Time the thread spent each state and cpu in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| cpu | INT | Id of the CPU. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_for_cpu_in_interval** . Time spent by CPU in each scheduling state in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| cpu | INT | CPU id. | +| ts | INT | Interval start. | +| dur | INT | Interval duration. | + +| Column | Type | Description | +|---|---|---| +| end_state | STRING | End state. From `sched.end_state`. | +| dur | INT | Duration in state. | + +
+ +
+ +## Package: intervals + +### intervals.overlap + +#### Macros + +
+ +**intervals_overlap_count** . Compute the distribution of the overlap of the given intervals over time. Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) counter, with the value corresponding to the number of intervals that overlap the given timestamp and interval until the next timestamp. + +
+ +
+ +
+ +Compute the distribution of the overlap of the given intervals over time. + +Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) +counter, with the value corresponding to the number of intervals that overlap +the given timestamp and interval until the next timestamp. +Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. + +| Argument | Type | Description | +|---|---|---| +| segments | TableOrSubquery | Table or subquery containing interval data. | +| ts_column | ColumnName | Column containing interval starts (usually `ts`). | +| dur_column | ColumnName | Column containing interval durations (usually `dur`). | + +
+ +
+ +## Package: time + +### time.conversion + +#### Functions + +
+ +**time_from_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_from_us** -\> INT. Converts a duration in microseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in microseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| micros | INT | Time duration in microseconds. | + +
+ +
+ +
+ +**time_from_ms** -\> INT. Converts a duration in millseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in millseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| millis | INT | Time duration in milliseconds. | + +
+ +
+ +
+ +**time_from_s** -\> INT. Converts a duration in seconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in seconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| seconds | INT | Time duration in seconds. | + +
+ +
+ +
+ +**time_from_min** -\> INT. Converts a duration in minutes to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in minutes to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| minutes | INT | Time duration in minutes. | + +
+ +
+ +
+ +**time_from_hours** -\> INT. Converts a duration in hours to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in hours to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| hours | INT | Time duration in hours. | + +
+ +
+ +
+ +**time_from_days** -\> INT. Converts a duration in days to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in days to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| days | INT | Time duration in days. | + +
+ +
+ +
+ +**time_to_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_us** -\> INT. Converts a duration in nanoseconds to microseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to microseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in microseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_ms** -\> INT. Converts a duration in nanoseconds to millseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to millseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in milliseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_s** -\> INT. Converts a duration in nanoseconds to seconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to seconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in seconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_min** -\> INT. Converts a duration in nanoseconds to minutes + +
+ +
+ +
+ +Converts a duration in nanoseconds to minutes. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in minutes. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_hours** -\> INT. Converts a duration in nanoseconds to hours + +
+ +
+ +
+ +Converts a duration in nanoseconds to hours. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in hours. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_days** -\> INT. Converts a duration in nanoseconds to days + +
+ +
+ +
+ +Converts a duration in nanoseconds to days. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in days. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +## Package: linux + +### linux.cpu.frequency + +#### Views/Tables + +
+ +**cpu_frequency_counters**. Counter information for each frequency change for each CPU + +
+ +TABLE +Counter information for each frequency change for each CPU. Finds each time +region where a CPU frequency is constant. + +| Column | Type | Description | +|---|---|---| +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter | +| dur | INT | Duration in which counter is constant and frequency doesn't change. | +| freq | INT | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | +| ucpu | INT | Unique CPU id. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
+ +### linux.cpu.idle + +#### Views/Tables + +
+ +**cpu_idle_counters**. Counter information for each idle state change for each CPU + +
+ +TABLE +Counter information for each idle state change for each CPU. Finds each time +region where a CPU idle state is constant. + +| Column | Type | Description | +|---|---|---| +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which the counter is contant and idle state doesn't change. | +| idle | INT | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
+ +### linux.cpu.idle_stats + +#### Views/Tables + +
+ +**cpu_idle_stats**. Aggregates cpu idle statistics per core. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| cpu | INT | CPU core number. | +| state | INT | CPU idle state (C-states). | +| count | INT | The count of entering idle state. | +| dur | INT | Total CPU core idle state duration in nanoseconds. | +| avg_dur | INT | Average CPU core idle state duration in nanoseconds. | +| idle_percent | FLOAT | Idle state percentage of non suspend time (C-states + P-states). | + +
+ +
+ +### linux.cpu.idle_time_in_state + +#### Views/Tables + +
+ +**cpu_idle_time_in_state_counters**. Counter information for sysfs cpuidle states. Tracks the percentage of time spent in each state between two timestamps, by dividing the incremental time spent in one state, by time all CPUS spent in any state. + +
+ +TABLE +Counter information for sysfs cpuidle states. +Tracks the percentage of time spent in each state between two timestamps, by +dividing the incremental time spent in one state, by time all CPUS spent in +any state. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Timestamp. | +| state_name | STRING | State name. | +| idle_percentage | DOUBLE | Percentage of time all CPUS spent in this state. | +| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | +| time_slice | INT | Time all CPUS spent in any state, in microseconds. | + +
+ +
+ +### linux.cpu.utilization.process + +#### Views/Tables + +
+ +**cpu_cycles_per_process**. Aggregated CPU statistics for each process. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Unique process id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +#### Table Functions + +
+ +**cpu_process_utilization_per_period** . Returns a table of process utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of process utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +upid \| INT \| Upid of the process. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_process_utilization_per_second** . Returns a table of process utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of process utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of the process. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_per_process_in_interval** . Aggregated CPU statistics for each process in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| upid | INT | Unique process id. Joinable with `process.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +### linux.cpu.utilization.slice + +#### Views/Tables + +
+ +**cpu_cycles_per_thread_slice**. CPU cycles per each slice. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +#### Table Functions + +
+ +**cpu_cycles_per_thread_slice_in_interval** . CPU cycles per each slice in interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +### linux.cpu.utilization.system + +#### Views/Tables + +
+ +**cpu_utilization_per_second**. Table with system utilization per second. Utilization is calculated by sum of average utilization of each CPU every second + +
+ +TABLE +Table with system utilization per second. +Utilization is calculated by sum of average utilization of each CPU every +second. For this reason first and last second might have lower then real +utilization. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles**. Aggregated CPU statistics for whole trace + +
+ +TABLE +Aggregated CPU statistics for whole trace. Results in only one row. + +| Column | Type | Description | +|---|---|---| +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +
+ +**cpu_cycles_per_cpu**. Aggregated CPU statistics for each CPU. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +#### Table Functions + +
+ +**cpu_utilization_per_period** . Returns a table of system utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of system utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_in_interval** . Aggregated CPU statistics in a provided interval + +
+ +
+ +
+ +Aggregated CPU statistics in a provided interval. Results in one row. +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Start of the interval. +dur \| INT \| Duration of the interval. + +| Column | Type | Description | +|---|---|---| +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +
+ +**cpu_cycles_per_cpu_in_interval** . Aggregated CPU statistics for each CPU in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | CPU number. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +### linux.cpu.utilization.thread + +#### Views/Tables + +
+ +**cpu_cycles_per_thread**. Aggregated CPU statistics for each thread. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| utid | INT | Unique thread id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +#### Table Functions + +
+ +**cpu_thread_utilization_per_period** . Returns a table of thread utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of thread utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +utid \| INT \| Utid of the thread. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_thread_utilization_per_second** . Returns a table of thread utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of thread utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +utid \| INT \| Utid of the thread. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_per_thread_in_interval** . Aggregated CPU statistics for each thread in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| utid | INT | Unique thread id. Joinable with `thread.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +### linux.devfreq + +#### Views/Tables + +
+ +**linux_devfreq_dsu_counter**. ARM DSU device frequency counters + +
+ +TABLE +ARM DSU device frequency counters. This table will only be populated on +traces collected with "devfreq/devfreq_frequency" ftrace event enabled, +and from ARM devices with the DSU (DynamIQ Shared Unit) hardware. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| dsu_freq | INT | Frequency in kHz of the device that corresponds to the counter. | + +
+ +
+ +#### Table Functions + +
+ +**linux_get_devfreq_counters** . Gets devfreq frequency counter based on device queried + +
+ +
+ +
+ +Gets devfreq frequency counter based on device queried. These counters will +only be available if the "devfreq/devfreq_frequency" ftrace event is enabled. +Argument \| Type \| Description +--- \| --- \| --- +device_name \| STRING \| Devfreq name to query for. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| freq | INT | Frequency in kHz of the device that corresponds to the counter. | + +
+ +
+ +### linux.memory.high_watermark + +#### Views/Tables + +
+ +**memory_rss_high_watermark_per_process**. For each process fetches the memory high watermark until or during timestamp. + +
+ +VIEW +For each process fetches the memory high watermark until or during +timestamp. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | +| process_name | STRING | Name of the process | +| rss_high_watermark | INT | Maximum `rss` value until now | + +
+ +
+ +### linux.memory.process + +#### Views/Tables + +
+ +**memory_rss_and_swap_per_process**. Memory metrics timeline for each process. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | +| process_name | STRING | Name of the process | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
+ +### linux.perf.samples + +#### Views/Tables + +
+ +**linux_perf_samples_summary_tree** . Table summarising the callstacks captured during all perf samples in the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. + +
+ +TABLE +Table summarising the callstacks captured during all +perf samples in the trace. + +Specifically, this table returns a tree containing all +the callstacks seen during the trace with `self_count` +equal to the number of samples with that frame as the +leaf and `cumulative_count` equal to the number of +samples with the frame anywhere in the tree. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | INT | The id of the parent callstack for this callstack. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
+ +### linux.perf.spe + +#### Views/Tables + +
+ +**linux_perf_spe_record**. Contains ARM Statistical Profiling Extension records + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Timestap when the operation was sampled | +| utid | INT | Thread the operation executed in | +| exception_level | STRING | Exception level the instruction was executed in | +| instruction_frame_id | INT | Instruction virtual address | +| operation | STRING | Type of operation sampled | +| data_virtual_address | LONG | The virtual address accessed by the operation (0 if no memory access was performed) | +| data_physical_address | LONG | The physical address accessed by the operation (0 if no memory access was performed) | +| total_latency | INT | Cycle count from the operation being dispatched for issue to the operation being complete. | +| issue_latency | INT | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | +| translation_latency | INT | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | +| data_source | STRING | Where the data returned for a load operation was sourced | +| exception_gen | BOOL | Operation generated an exception | +| retired | BOOL | Operation architecturally retired | +| l1d_access | BOOL | Operation caused a level 1 data cache access | +| l1d_refill | BOOL | Operation caused a level 1 data cache refill | +| tlb_access | BOOL | Operation caused a TLB access | +| tlb_refill | BOOL | Operation caused a TLB refill involving at least one translation table walk | +| not_taken | BOOL | Conditional instruction failed its condition code check | +| mispred | BOOL | Whether a branch caused a correction to the predicted program flow | +| llc_access | BOOL | Operation caused a last level data or unified cache access | +| llc_refill | BOOL | Whether the operation could not be completed by the last level data cache (or any above) | +| remote_access | BOOL | Operation caused an access to another socket in a multi-socket system | +| alignment | BOOL | Operation that incurred additional latency due to the alignment of the address and the size of the data being accessed | +| tme_transaction | BOOL | Whether the operation executed in transactional state | +| sve_partial_pred | BOOL | SVE or SME operation with at least one false element in the governing predicate(s) | +| sve_empty_pred | BOOL | SVE or SME operation with no true element in the governing predicate(s) | +| l2d_access | BOOL | Whether a load operation caused a cache access to at least the level 2 data or unified cache | +| l2d_hit | BOOL | Whether a load operation accessed and missed the level 2 data or unified cache. Not set for accesses that are satisfied from refilling data of a previous miss | +| cache_data_modified | BOOL | Whether a load operation accessed modified data in a cache | +| recenty_fetched | BOOL | Wheter a load operation hit a recently fetched line in a cache | +| data_snooped | BOOL | Whether a load operation snooped data from a cache outside the cache hierarchy of this core | + +
+ +
+ +### linux.threads + +#### Views/Tables + +
+ +**linux_kernel_threads**. All kernel threads of the trace + +
+ +TABLE +All kernel threads of the trace. As kernel threads are processes, provides +also process data. + +| Column | Type | Description | +|---|---|---| +| upid | INT | Upid of kernel thread. Alias of \|process.upid\|. | +| utid | INT | Utid of kernel thread. Alias of \|thread.utid\|. | +| pid | INT | Pid of kernel thread. Alias of \|process.pid\|. | +| tid | INT | Tid of kernel thread. Alias of \|process.pid\|. | +| process_name | STRING | Name of kernel process. Alias of \|process.name\|. | +| thread_name | STRING | Name of kernel thread. Alias of \|thread.name\|. | +| machine_id | INT | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of \|process.machine_id\|. | + +
+ +
+ +## Package: android + +### android.anrs + +#### Views/Tables + +
+ +**android_anrs**. List of all ANRs that occurred in the trace (one row per ANR). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that triggered the ANR. | +| pid | INT | PID of the process that triggered the ANR. | +| upid | INT | UPID of the process that triggered the ANR. | +| error_id | STRING | UUID of the ANR (generated on the platform). | +| ts | INT | Timestamp of the ANR. | +| subject | STRING | Subject line of the ANR. | + +
+ +
+ +### android.app_process_starts + +#### Views/Tables + +
+ +**android_app_process_starts**. All app cold starts with information about their cold start reason: broadcast, service, activity or provider. + +
+ +TABLE +All app cold starts with information about their cold start reason: +broadcast, service, activity or provider. + +| Column | Type | Description | +|---|---|---| +| start_id | INT | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | +| id | INT | Slice id of intent received in the app. | +| track_id | INT | Track id of the intent received in the app. | +| process_name | STRING | Name of the process receiving the intent. | +| pid | INT | Pid of the process receiving the intent. | +| upid | INT | Upid of the process receiving the intent. | +| intent | STRING | Intent action or component responsible for the cold start. | +| reason | STRING | Process start reason: activity, broadcast, service or provider. | +| proc_start_ts | INT | Timestamp the process start was dispatched from system_server. | +| proc_start_dur | INT | Duration to dispatch the process start from system_server. | +| bind_app_ts | INT | Timestamp the bindApplication started in the app. | +| bind_app_dur | INT | Duration to complete bindApplication in the app. | +| intent_ts | INT | Timestamp the Intent was received in the app. | +| intent_dur | INT | Duration to handle intent in the app. | +| total_dur | INT | Total duration from proc_start dispatched to intent completed. | + +
+ +
+ +### android.auto.multiuser + +#### Views/Tables + +
+ +**android_auto_multiuser_timing**. Time elapsed between the latest user start and the specific end event like package startup(ex carlauncher) or previous user stop. + +
+ +TABLE +Time elapsed between the latest user start +and the specific end event +like package startup(ex carlauncher) or previous user stop. + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Id of the started android user | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | + +
+ +
+ +
+ +**android_auto_multiuser_timing_with_previous_user_resource_usage** . This table extends `android_auto_multiuser_timing` table with previous user resource usage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Start user id | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | +| user_id | INT | User id | +| total_cpu_time | LONG | Total CPU time for a user | +| total_memory_usage_kb | LONG | Total memory user for a user | + +
+ +
+ +### android.battery + +#### Views/Tables + +
+ +**android_battery_charge**. Battery charge at timestamp. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp. | +| current_avg_ua | DOUBLE | Current average micro ampers. | +| capacity_percent | DOUBLE | Current capacity percentage. | +| charge_uah | DOUBLE | Current charge in micro ampers. | +| current_ua | DOUBLE | Current micro ampers. | + +
+ +
+ +### android.battery_stats + +#### Views/Tables + +
+ +**android_battery_stats_state**. View of human readable battery stats counter-based states + +
+ +VIEW +View of human readable battery stats counter-based states. These are recorded +by BatteryStats as a bitmap where each 'category' has a unique value at any +given time. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| value | INT | The counter value as a number. | +| value_name | STRING | The counter value as a human-readable string. | + +
+ +
+ +
+ +**android_battery_stats_event_slices**. View of slices derived from battery_stats events + +
+ +VIEW +View of slices derived from battery_stats events. Battery stats records all +events as instants, however some may indicate whether something started or +stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground +apps or long wakes include these details and allow drawing slices between +instant events found in a trace. + +For example, we may see an event like the following on 'battery_stats.top': + + -top=10215:"com.google.android.apps.nexuslauncher" + +This view will find the associated start ('+top') with the matching suffix +(everything after the '=') to construct a slice. It computes the timestamp +and duration from the events and extract the details as follows: + + track_name='battery_stats.top' + str_value='com.google.android.apps.nexuslauncher' + int_value=10215 + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| str_value | STRING | String value. | +| int_value | INT | Int value. | + +
+ +
+ +#### Functions + +
+ +**android_battery_stats_counter_to_string** -\> STRING. Converts a battery_stats counter value to human readable string. + +
+ +
+ +
+ +Returns STRING: The human-readable name for the counter value. + +| Argument | Type | Description | +|---|---|---| +| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | +| value | FLOAT | The counter value. | + +
+ +
+ +### android.binder + +#### Views/Tables + +
+ +**android_binder_metrics_by_process**. Count Binder transactions per process. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that started the binder transaction. | +| pid | INT | PID of the process that started the binder transaction. | +| slice_name | STRING | Name of the slice with binder transaction. | +| event_count | INT | Number of binder transactions in process in slice. | + +
+ +
+ +
+ +**android_sync_binder_thread_state_by_txn**. Aggregated thread_states on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the thread state durations. The \|thread_state_type\| column represents whether a given 'aggregated thread_state' row is on the client or server side + +
+ +VIEW +Aggregated thread_states on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the thread state durations. +The \|thread_state_type\| column represents whether a given 'aggregated thread_state' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client timestamp | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server timestamp | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| thread_state | STRING | a thread_state that occurred in the txn | +| thread_state_dur | INT | aggregated dur of the \|thread_state\| in the txn | +| thread_state_count | INT | aggregated count of the \|thread_state\| in the txn | + +
+ +
+ +
+ +**android_sync_binder_blocked_functions_by_txn**. Aggregated blocked_functions on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the kernel blocked function durations. The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' row is on the client or server side + +
+ +VIEW +Aggregated blocked_functions on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the kernel blocked function durations. +The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client ts | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server ts | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| blocked_function | STRING | blocked kernel function in a thread state | +| blocked_function_dur | INT | aggregated dur of the \|blocked_function\| in the txn | +| blocked_function_count | INT | aggregated count of the \|blocked_function\| in the txn | + +
+ +
+ +
+ +**android_binder_txns**. Breakdown binder transactions per txn. It returns data about the client and server ends of every binder transaction async. + +
+ +TABLE +Breakdown binder transactions per txn. +It returns data about the client and server ends of every binder transaction async. + +| Column | Type | Description | +|---|---|---| +| aidl_name | STRING | name of the binder interface if existing. | +| aidl_ts | INT | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | +| aidl_dur | INT | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | +| binder_txn_id | INT | slice id of the binder txn. | +| client_process | STRING | name of the client process. | +| client_thread | STRING | name of the client thread. | +| client_upid | INT | Upid of the client process. | +| client_utid | INT | Utid of the client thread. | +| client_tid | INT | Tid of the client thread. | +| client_pid | INT | Pid of the client thread. | +| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | +| client_ts | INT | timestamp of the client txn. | +| client_dur | INT | wall clock dur of the client txn. | +| binder_reply_id | INT | slice id of the binder reply. | +| server_process | STRING | name of the server process. | +| server_thread | STRING | name of the server thread. | +| server_upid | INT | Upid of the server process. | +| server_utid | INT | Utid of the server thread. | +| server_tid | INT | Tid of the server thread. | +| server_pid | INT | Pid of the server thread. | +| server_ts | INT | timestamp of the server txn. | +| server_dur | INT | wall clock dur of the server txn. | +| client_oom_score | INT | oom score of the client process at the start of the txn. | +| server_oom_score | INT | oom score of the server process at the start of the reply. | +| is_sync | BOOL | whether the txn is synchronous or async (oneway). | +| client_monotonic_dur | INT | monotonic clock dur of the client txn. | +| server_monotonic_dur | INT | monotonic clock dur of the server txn. | +| client_package_version_code | INT | Client package version_code. | +| server_package_version_code | INT | Server package version_code. | +| is_client_package_debuggable | INT | Whether client package is debuggable. | +| is_server_package_debuggable | INT | Whether server package is debuggable. | + +
+ +
+ +#### Table Functions + +
+ +**android_binder_outgoing_graph** . Returns a DAG of all outgoing binder txns from a process. The roots of the graph are the threads making the txns and the graph flows from: thread -\> server_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a DAG of all outgoing binder txns from a process. +The roots of the graph are the threads making the txns and the graph flows from: +thread -\> server_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an outgoing graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of outgoing binder txns. | + +
+ +
+ +
+ +**android_binder_incoming_graph** . Returns a DAG of all incoming binder txns from a process. The roots of the graph are the clients making the txns and the graph flows from: client_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a DAG of all incoming binder txns from a process. +The roots of the graph are the clients making the txns and the graph flows from: +client_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an incoming graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of incoming binder txns. | + +
+ +
+ +
+ +**android_binder_graph** . Returns a graph of all binder txns in a trace. The nodes are client_process and server_process. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a graph of all binder txns in a trace. +The nodes are client_process and server_process. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +min_client_oom_score \| INT \| Matches txns from client_processes greater than or equal to the OOM score. +max_client_oom_score \| INT \| Matches txns from client_processes less than or equal to the OOM score. +min_server_oom_score \| INT \| Matches txns to server_processes greater than or equal to the OOM score. +max_server_oom_score \| INT \| Matches txns to server_processes less than or equal to the OOM score. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of binder txns. | + +
+ +
+ +### android.binder_breakdown + +#### Views/Tables + +
+ +**android_binder_server_breakdown**. Server side binder breakdowns per transactions per txn. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder reply with a single reason. | +| dur | INT | Duration of an exclusive interval during the binder reply with a single reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | + +
+ +
+ +
+ +**android_binder_client_breakdown**. Client side binder breakdowns per transactions per txn. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder txn with a single latency reason. | +| dur | INT | Duration of an exclusive interval during the binder txn with a single latency reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | + +
+ +
+ +### android.cpu.cluster_type + +#### Views/Tables + +
+ +**android_cpu_cluster_mapping**. Stores the mapping of a cpu to its cluster type - e.g + +
+ +TABLE +Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. +This cluster type is determined by initially using cpu_capacity from sysfs +and grouping clusters with identical capacities, ordered by size. +In the case that capacities are not present, max frequency is used instead. +If nothing is avaiable, NULL is returned. + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Alias of `cpu.ucpu`. | +| cpu | INT | Alias of `cpu.cpu`. | +| cluster_type | STRING | The cluster type of the CPU. | + +
+ +
+ +### android.device + +#### Views/Tables + +
+ +**android_device_name**. Extract name of the device based on metadata from the trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Device name. | + +
+ +
+ +### android.dvfs + +#### Views/Tables + +
+ +**android_dvfs_counters**. Dvfs counter with duration. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| ts | INT | Timestamp when counter value changed. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | + +
+ +
+ +
+ +**android_dvfs_counter_stats**. Aggregates dvfs counter slice for statistic. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name on which all the other values are aggregated on. | +| max | DOUBLE | Max of all counter values for the counter name. | +| min | DOUBLE | Min of all counter values for the counter name. | +| dur | INT | Duration between the first and last counter value for the counter name. | +| wgt_avg | FLOAT | Weighted avergate of all the counter values for the counter name. | + +
+ +
+ +
+ +**android_dvfs_counter_residency**. Aggregates dvfs counter slice for residency + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | +| pct | DOUBLE | Counter duration as a percentage of total duration. | + +
+ +
+ +### android.frames.jank_type + +#### Functions + +
+ +**android_is_sf_jank_type** -\> BOOL. Categorizes whether the jank was caused by Surface Flinger + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents sf jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +
+ +**android_is_app_jank_type** -\> BOOL. Categorizes whether the jank was caused by the app + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents app jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +### android.frames.per_frame_metrics + +#### Views/Tables + +
+ +**android_frames_overrun**. The amount by which each frame missed of hit its deadline + +
+ +TABLE +The amount by which each frame missed of hit its deadline. Negative if the +deadline was not missed. Frames are considered janky if `overrun` is +positive. +Calculated as the difference between the end of the +`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the +frame. +Availability: from S (API 31). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | + +
+ +
+ +
+ +**android_frames_ui_time**. How much time did the frame's Choreographer callbacks take. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| ui_time | INT | UI time duration | + +
+ +
+ +
+ +**android_app_vsync_delay_per_frame**. App Vsync delay for a frame + +
+ +TABLE +App Vsync delay for a frame. The time between the VSYNC-app signal and the +start of Choreographer work. +Calculated as time difference between the actual frame start (from +`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` +slice. +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | App VSYNC delay. | + +
+ +
+ +
+ +**android_cpu_time_per_frame** . How much time did the frame take across the UI Thread + RenderThread. Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice duration and summed durations of all `DrawFrame` slices associated with this frame. Availability: from N (API 24). For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +How much time did the frame take across the UI Thread + RenderThread. +Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice +duration and summed durations of all `DrawFrame` slices associated with this +frame. +Availability: from N (API 24). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | +| do_frame_dur | INT | Duration of `Choreographer#doFrame` slice. | +| draw_frame_dur | INT | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | +| cpu_time | INT | CPU time across the UI Thread + RenderThread. | + +
+ +
+ +
+ +**android_frame_stats**. Aggregated stats of the frame. For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +Aggregated stats of the frame. + +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | +| cpu_time | INT | How much time did the frame take across the UI Thread + RenderThread. | +| ui_time | INT | How much time did the frame's Choreographer callbacks take. | +| was_jank | BOOL | Was frame janky. | +| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | +| was_big_jank | BOOL | CPU time of the frame took over 50ms. | +| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | + +
+ +
+ +### android.frames.timeline + +#### Views/Tables + +
+ +**android_frames_choreographer_do_frame** . All of the `Choreographer#doFrame` slices with their frame id. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| ui_thread_utid | INT | Utid of the UI thread | +| upid | INT | Upid of application process | +| ts | INT | Timestamp of the slice. | + +
+ +
+ +
+ +**android_frames_draw_frame** . All of the `DrawFrame` slices with their frame id and render thread. There might be multiple DrawFrames slices for a single vsync (frame id). This happens when we are drawing multiple layers (e.g + +
+ +TABLE +All of the `DrawFrame` slices with their frame id and render thread. +There might be multiple DrawFrames slices for a single vsync (frame id). +This happens when we are drawing multiple layers (e.g. status bar and +notifications). + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| render_thread_utid | INT | Utid of the render thread | +| upid | INT | Upid of application process | + +
+ +
+ +
+ +**android_frames**. All slices related to one frame + +
+ +TABLE +All slices related to one frame. Aggregates `Choreographer#doFrame`, +`DrawFrame`, `actual_frame_timeline_slice` and +`expected_frame_timeline_slice` slices. +See https://perfetto.dev/docs/data-sources/frametimeline for details. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | +| dur | INT | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | +| actual_frame_timeline_count | INT | Count of slices in `actual_frame_timeline_slice` related to this frame. | +| expected_frame_timeline_count | INT | Count of slices in `expected_frame_timeline_slice` related to this frame. | + +
+ +
+ +#### Table Functions + +
+ +**android_first_frame_after** . Returns first frame after the provided timestamp + +
+ +
+ +
+ +Returns first frame after the provided timestamp. The returning table has at +most one row. +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Timestamp. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | +| dur | INT | Duration of the frame. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | + +
+ +
+ +### android.freezer + +#### Views/Tables + +
+ +**android_freezer_events**. All frozen processes and their frozen duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Upid of frozen process | +| pid | INT | Pid of frozen process | +| ts | INT | Timestamp process was frozen. | +| dur | INT | Duration process was frozen for. | +| unfreeze_reason_int | INT | Unfreeze reason Integer. | +| unfreeze_reason_str | STRING | Unfreeze reason String. | + +
+ +
+ +### android.garbage_collection + +#### Views/Tables + +
+ +**android_garbage_collection_events**. All Garbage collection events with a breakdown of the time spent and heap reclaimed. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| tid | INT | Tid of thread running garbage collection. | +| pid | INT | Pid of process running garbage collection. | +| utid | INT | Utid of thread running garbage collection. | +| upid | INT | Upid of process running garbage collection. | +| thread_name | STRING | Name of thread running garbage collection. | +| process_name | STRING | Name of process running garbage collection. | +| gc_type | STRING | Type of garbage collection. | +| is_mark_compact | INT | Whether gargage collection is mark compact or copying. | +| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | +| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | +| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | +| gc_id | INT | Garbage collection id. | +| gc_ts | INT | Garbage collection timestamp. | +| gc_dur | INT | Garbage collection wall duration. | +| gc_running_dur | INT | Garbage collection duration spent executing on CPU. | +| gc_runnable_dur | INT | Garbage collection duration spent waiting for CPU. | +| gc_unint_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel on IO. | +| gc_unint_non_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel without IO. | +| gc_int_dur | INT | Garbage collection duration spent waiting in interruptible sleep. | + +
+ +
+ +### android.gpu.frequency + +#### Views/Tables + +
+ +**android_gpu_frequency**. GPU frequency counter per GPU. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| gpu_id | INT | GPU id. Joinable with `gpu_counter_track.gpu_id`. | +| gpu_freq | INT | GPU frequency | + +
+ +
+ +### android.gpu.memory + +#### Views/Tables + +
+ +**android_gpu_memory_per_process**. Counter for GPU memory per process with duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| gpu_memory | INT | GPU memory | + +
+ +
+ +### android.input + +#### Views/Tables + +
+ +**android_input_events**. All input events with round trip latency breakdown + +
+ +TABLE +All input events with round trip latency breakdown. Input delivery is socket based and every +input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure +latencies between: +1. Input dispatch event sent from OS. +2. Input dispatch event received in app. +3. Input ACK event sent from app. +4. Input ACk event received in OS. + +| Column | Type | Description | +|---|---|---| +| dispatch_latency_dur | INT | Duration from input dispatch to input received. | +| handling_latency_dur | INT | Duration from input received to input ACK sent. | +| ack_latency_dur | INT | Duration from input ACK sent to input ACK recieved. | +| total_latency_dur | INT | Duration from input dispatch to input event ACK received. | +| end_to_end_latency_dur | INT | Duration from input read to frame present time. Null if an input event has no associated frame event. | +| tid | INT | Tid of thread receiving the input event. | +| thread_name | STRING | Name of thread receiving the input event. | +| pid | INT | Pid of process receiving the input event. | +| process_name | STRING | Name of process receiving the input event. | +| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | +| event_action | STRING | Input event action. | +| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | +| event_channel | STRING | Input event channel name. | +| input_event_id | STRING | Unique identifier for the input event. | +| read_time | INT | Timestamp input event was read by InputReader. | +| dispatch_track_id | INT | Thread track id of input event dispatching thread. | +| dispatch_ts | INT | Timestamp input event was dispatched. | +| dispatch_dur | INT | Duration of input event dispatch. | +| receive_track_id | INT | Thread track id of input event receiving thread. | +| receive_ts | INT | Timestamp input event was received. | +| receive_dur | INT | Duration of input event receipt. | +| frame_id | INT | Vsync Id associated with the input. Null if an input event has no associated frame event. | + +
+ +
+ +
+ +**android_key_events**. Key events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_motion_events**. Motion events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_input_event_dispatch**. Input event dispatching information in Android (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | Event ID of the input event that was dispatched | +| arg_set_id | INT | Extra args parsed from the proto message | +| vsync_id | INT | Vsync ID that identifies the state of the windows during which the dispatch decision was made | +| window_id | INT | Window ID of the window receiving the event | + +
+ +
+ +### android.job_scheduler + +#### Views/Tables + +
+ +**android_job_scheduler_events**. All scheduled jobs and their latencies. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| job_id | INT | Id of the scheduled job assigned by the app developer. | +| uid | INT | Uid of the process running the scheduled job. | +| package_name | STRING | Package name of the process running the scheduled job. | +| job_service_name | STRING | Service component name of the scheduled job. | +| track_id | INT | Thread track id of the job scheduler event slice. | +| id | INT | Slice id of the job scheduler event slice. | +| ts | INT | Timestamp the job was scheduled. | +| dur | INT | Duration of the scheduled job. | + +
+ +
+ +### android.memory.dmabuf + +#### Views/Tables + +
+ +**android_dmabuf_allocs**. Track dmabuf allocations, re-attributing gralloc allocations to their source (if binder transactions to gralloc are recorded). + +
+ +TABLE +Track dmabuf allocations, re-attributing gralloc allocations to their source +(if binder transactions to gralloc are recorded). + +| Column | Type | Description | +|---|---|---| +| ts | INT | timestamp of the allocation | +| buf_size | INT | allocation size (will be negative for release) | +| inode | INT | dmabuf inode | +| utid | INT | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | +| tid | INT | tid of thread responsible for the allocation | +| thread_name | STRING | thread name | +| upid | INT | upid of process responsible for the allocation (matches utid) | +| pid | INT | pid of process responsible for the allocation | +| process_name | STRING | process name | + +
+ +
+ +### android.memory.heap_graph.dominator_tree + +#### Views/Tables + +
+ +**heap_graph_dominator_tree**. All reachable heap graph objects, their immediate dominators and summary of their dominated sets. The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. Each reachable object is a node in the dominator tree, their immediate dominator is their parent node in the tree, and their dominated set is all their descendants in the tree + +
+ +TABLE +All reachable heap graph objects, their immediate dominators and summary of +their dominated sets. +The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. +Each reachable object is a node in the dominator tree, their immediate +dominator is their parent node in the tree, and their dominated set is all +their descendants in the tree. All size information come from the +heap_graph_object prelude table. + +| Column | Type | Description | +|---|---|---| +| id | INT | Heap graph object id. | +| idom_id | INT | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | +| dominated_obj_count | INT | Count of all objects dominated by this object, self inclusive. | +| dominated_size_bytes | INT | Total self_size of all objects dominated by this object, self inclusive. | +| dominated_native_size_bytes | INT | Total native_size of all objects dominated by this object, self inclusive. | +| depth | INT | Depth of the object in the dominator tree. Depth of root objects are 1. | + +
+ +
+ +### android.memory.heap_graph.heap_graph_class_aggregation + +#### Views/Tables + +
+ +**android_heap_graph_class_aggregation**. Class-level breakdown of the java heap. Per type name aggregates the object stats and the dominator tree stats. + +
+ +TABLE +Class-level breakdown of the java heap. +Per type name aggregates the object stats and the dominator tree stats. + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid | +| graph_sample_ts | INT | Heap dump timestamp | +| type_id | INT | Class type id | +| type_name | STRING | Class name (deobfuscated if available) | +| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | +| obj_count | INT | Count of class instances | +| size_bytes | INT | Size of class instances | +| native_size_bytes | INT | Native size of class instances | +| reachable_obj_count | INT | Count of reachable class instances | +| reachable_size_bytes | INT | Size of reachable class instances | +| reachable_native_size_bytes | INT | Native size of reachable class instances | +| dominated_obj_count | INT | Count of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_size_bytes | INT | Size of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_native_size_bytes | INT | Native size of all objects dominated by instances of this class Only applies to reachable objects | + +
+ +
+ +### android.memory.process + +#### Views/Tables + +
+ +**memory_oom_score_with_rss_and_swap_per_process**. Process memory and it's OOM adjuster scores + +
+ +TABLE +Process memory and it's OOM adjuster scores. Detects transitions, each new +interval means that either the memory or OOM adjuster score of the process changed. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score or memory of the process changed | +| dur | INT | Duration until the next oom_adj score or memory change of the process. | +| score | INT | oom adjuster score of the process. | +| bucket | STRING | oom adjuster bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| pid | INT | Pid of the process having an oom_adj update. | +| oom_adj_id | INT | Slice of the latest oom_adj update in the system_server. Alias of `slice.id`. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
+ +### android.monitor_contention + +#### Views/Tables + +
+ +**android_monitor_contention**. Contains parsed monitor contention slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain**. Contains parsed monitor contention slices with the parent-child relationships. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| parent_id | INT | Id of monitor contention slice blocking this contention. | +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | +| child_id | INT | Id of monitor contention slice blocked by this contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state**. Note that we only span join the duration where the lock was actually held and contended. This can be less than the duration the lock was 'waited on' when a different waiter acquired the lock earlier than the first waiter. + +
+ +TABLE +Note that we only span join the duration where the lock was actually held and contended. +This can be less than the duration the lock was 'waited on' when a different waiter acquired the +lock earlier than the first waiter. + +| Column | Type | Description | +|---|---|---| +| parent_id | None | Id of slice blocking the blocking_thread. | +| blocking_method | None | Name of the method holding the lock. | +| blocked_methhod | None | Name of the method trying to acquire the lock. | +| short_blocking_method | None | Blocking_method without arguments and return types. | +| short_blocked_method | None | Blocked_method without arguments and return types. | +| blocking_src | None | File location of blocking_method in form . | +| blocked_src | None | File location of blocked_method in form . | +| waiter_count | None | Zero indexed number of threads trying to acquire the lock. | +| blocking_utid | None | Utid of the blocking \|thread_state\|. | +| blocking_thread_name | None | Thread name of thread holding the lock. | +| upid | None | Upid of process experiencing lock contention. | +| process_name | None | Process name of process experiencing lock contention. | +| id | None | Slice id of lock contention. | +| ts | None | Timestamp of the blocking \|thread_state\|. | +| dur | None | Wall clock duration of lock contention. | +| monotonic_dur | None | Monotonic clock duration of lock contention. | +| track_id | None | Thread track id of blocked thread. | +| is_blocked_main_thread | None | Whether the blocked thread is the main thread. | +| is_blocking_main_thread | None | Whether the blocking thread is the main thread. | +| binder_reply_id | None | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | None | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | None | Tid of binder reply slice if lock contention was part of a binder txn. | +| state | None | Thread state of the blocking thread. | +| blocked_function | None | Blocked kernel function of the blocking thread. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state_by_txn**. Aggregated thread_states on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention slice, it returns the aggregated sum of all the thread states on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated thread_states on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention slice, it returns the aggregated sum of all the thread states on the +blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| thread_state | STRING | A \|thread_state\| that occurred in the blocking thread during the contention. | +| thread_state_dur | INT | Total time the blocking thread spent in the \|thread_state\| during contention. | +| thread_state_count | INT | Count of all times the blocking thread entered \|thread_state\| during the contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_blocked_functions_by_txn**. Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention, it returns the aggregated sum of all the kernel blocked function durations on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention, it returns the aggregated sum of all the kernel +blocked function durations on the blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | +| blocked_function_dur | INT | Total time the blocking thread spent in the \|blocked_function\| during the contention. | +| blocked_function_count | INT | Count of all times the blocking thread executed the \|blocked_function\| during the contention. | + +
+ +
+ +#### Functions + +
+ +**android_extract_android_monitor_contention_blocking_thread** -\> STRING. Extracts the blocking thread from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_tid** -\> INT. Extracts the blocking thread tid from a slice name + +
+ +
+ +
+ +Returns INT: Blocking thread tid + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_method** -\> STRING. Extracts the blocking method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocking_method** -\> STRING. Extracts a shortened form of the blocking method name from a slice name. The shortened form discards the parameter and return types. + +
+ +
+ +
+ +Extracts a shortened form of the blocking method name from a slice name. +The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_method** -\> STRING. Extracts the monitor contention blocked method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocked_method** -\> STRING. Extracts a shortened form of the monitor contention blocked method name from a slice name + +
+ +
+ +
+ +Extracts a shortened form of the monitor contention blocked method name +from a slice name. The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_waiter_count** -\> INT. Extracts the number of waiters on the monitor from a slice name + +
+ +
+ +
+ +Returns INT: Count of waiters on the lock + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_src** -\> STRING. Extracts the monitor contention blocking source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_src** -\> STRING. Extracts the monitor contention blocked source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +#### Table Functions + +
+ +**android_monitor_contention_graph** . Returns a DAG of all Java lock contentions in a process. Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. + +
+ +
+ +
+ +Returns a DAG of all Java lock contentions in a process. +Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. Argument \| Type \| Description --- \| --- \| --- upid \| INT \| Upid of process to generate a lock graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of lock graph. | + +
+ +
+ +### android.network_packets + +#### Views/Tables + +
+ +**android_network_packets**. Android network packet events (from android.network_packets data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | Duration (non-zero only in aggregate events) | +| track_name | STRING | The track name (interface and direction) | +| package_name | STRING | Traffic package source (or uid=$X if not found) | +| iface | STRING | Traffic interface name (linux interface name) | +| direction | STRING | Traffic direction ('Transmitted' or 'Received') | +| packet_count | INT | Number of packets in this event | +| packet_length | INT | Number of bytes in this event (wire size) | +| packet_transport | STRING | Transport used for traffic in this event | +| packet_tcp_flags | INT | TCP flags used by tcp frames in this event | +| socket_tag | STRING | The Android traffic tag of the network socket | +| socket_uid | INT | The Linux user id of the network socket | +| local_port | INT | The local port number (for udp or tcp only) | +| remote_port | INT | The remote port number (for udp or tcp only) | +| packet_icmp_type | INT | 1-byte ICMP type identifier. | +| packet_icmp_code | INT | 1-byte ICMP code identifier. | +| packet_tcp_flags_int | INT | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | +| socket_tag_int | INT | Packet's socket tag as an integer. | + +
+ +
+ +### android.oom_adjuster + +#### Views/Tables + +
+ +**android_oom_adj_intervals**. All oom adj state intervals across all processes along with the reason for the state update. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score of the process changed | +| dur | INT | Duration until the next oom_adj score change of the process. | +| score | INT | oom_adj score of the process. | +| bucket | STRING | oom_adj bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| oom_adj_id | INT | Slice id of the latest oom_adj update in the system_server. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track id of the latest oom_adj update in the system_server | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | + +
+ +
+ +#### Functions + +
+ +**android_oom_adj_score_to_bucket_name** -\> STRING. Converts an oom_adj score Integer to String sample name. One of: cached, background, job, foreground_service, bfgs, foreground and system. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String sample name. +One of: cached, background, job, foreground_service, bfgs, foreground and +system. +Returns STRING: Returns the sample bucket based on the oom score. + +| Argument | Type | Description | +|---|---|---| +| oom_score | INT | `oom_score` value | + +
+ +
+ +
+ +**android_oom_adj_score_to_detailed_bucket_name** -\> STRING. Converts an oom_adj score Integer to String bucket name. Deprecated: use `android_oom_adj_score_to_bucket_name` instead. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String bucket name. +Deprecated: use `android_oom_adj_score_to_bucket_name` instead. +Returns STRING: Returns the oom_adj bucket. + +| Argument | Type | Description | +|---|---|---| +| value | INT | oom_adj score. | +| android_appid | INT | android_app id of the process. | + +
+ +
+ +### android.power_rails + +#### Views/Tables + +
+ +**android_power_rails_counters**. Android power rails counters data. For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm NOTE: Requires dedicated hardware - table is only populated on Pixels. + +
+ +TABLE +Android power rails counters data. +For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm +NOTE: Requires dedicated hardware - table is only populated on Pixels. + +| Column | Type | Description | +|---|---|---| +| id | INT | `counter.id` | +| ts | INT | Timestamp of the energy measurement. | +| dur | INT | Time until the next energy measurement. | +| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | +| raw_power_rail_name | STRING | Raw power rail name. | +| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | +| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | +| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | +| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | +| track_id | INT | Power rail track id. Alias of `counter_track.id`. | +| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | + +
+ +
+ +### android.process_metadata + +#### Views/Tables + +
+ +**android_process_metadata**. Data about packages running on the process. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid. | +| pid | INT | Process pid. | +| process_name | STRING | Process name. | +| uid | INT | Android app UID. | +| shared_uid | BOOL | Whether the UID is shared by multiple packages. | +| package_name | STRING | Name of the packages running in this process. | +| version_code | INT | Package version code. | +| debuggable | INT | Whether package is debuggable. | + +
+ +
+ +### android.screenshots + +#### Views/Tables + +
+ +**android_screenshots**. Screenshot slices, used in perfetto UI. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| ts | INT | Slice timestamp. | +| dur | INT | Slice duration, should be typically 0 since screeenshot slices are of instant type. | +| name | STRING | Slice name. | + +
+ +
+ +### android.services + +#### Views/Tables + +
+ +**android_service_bindings**. All service bindings from client app to server app. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| client_oom_score | INT | OOM score of client process making the binding. | +| client_process | STRING | Name of client process making the binding. | +| client_thread | STRING | Name of client thread making the binding. | +| client_pid | INT | Pid of client process making the binding. | +| client_tid | INT | Tid of client process making the binding. | +| client_upid | INT | Upid of client process making the binding. | +| client_utid | INT | Utid of client thread making the binding. | +| client_ts | INT | Timestamp the client process made the request. | +| client_dur | INT | Duration of the client binding request. | +| server_oom_score | INT | OOM score of server process getting bound to. | +| server_process | STRING | Name of server process getting bound to | +| server_thread | STRING | Name of server thread getting bound to. | +| server_pid | INT | Pid of server process getting bound to. | +| server_tid | INT | Tid of server process getting bound to. | +| server_upid | INT | Upid of server process getting bound to. | +| server_utid | INT | Utid of server process getting bound to. | +| server_ts | INT | Timestamp the server process got bound to. | +| server_dur | INT | Duration of the server process handling the binding. | +| token | STRING | Unique binder identifier for the Service binding. | +| act | STRING | Intent action name for the service binding. | +| cmp | STRING | Intent component name for the service binding. | +| flg | STRING | Intent flag for the service binding. | +| bind_seq | INT | Monotonically increasing id for the service binding. | + +
+ +
+ +### android.slices + +#### Functions + +
+ +**android_standardize_slice_name** -\> STRING. Some slice names have params in them + +
+ +
+ +
+ +Some slice names have params in them. This functions removes them to make it +possible to aggregate by name. +Some examples are: +- Lock/monitor contention slices. The name includes where the lock +contention is in the code. That part is removed. +- DrawFrames/ooFrame. The name also includes the frame number. +- Apk/oat/dex loading: The name of the apk is removed +Returns STRING: Simplified name. + +| Argument | Type | Description | +|---|---|---| +| name | STRING | The raw slice name. | + +
+ +
+ +### android.startup.startups + +#### Views/Tables + +
+ +**android_startups**. All activity startups in the trace by startup id. Populated by different scripts depending on the platform version/contents. + +
+ +TABLE +All activity startups in the trace by startup id. +Populated by different scripts depending on the platform version/contents. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of startup start. | +| ts_end | INT | Timestamp of startup end. | +| dur | INT | Startup duration. | +| package | STRING | Package name. | +| startup_type | STRING | Startup type. | + +
+ +
+ +
+ +**android_startup_processes**. Maps a startup to the set of processes that handled the activity start. The vast majority of cases should be a single process + +
+ +TABLE +Maps a startup to the set of processes that handled the activity start. + +The vast majority of cases should be a single process. However it is +possible that the process dies during the activity startup and is respawned. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| upid | INT | Upid of process on which activity started. | +| startup_type | STRING | Type of the startup. | + +
+ +
+ +
+ +**android_startup_threads**. Maps a startup to the set of threads on processes that handled the activity start. + +
+ +VIEW +Maps a startup to the set of threads on processes that handled the +activity start. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of start. | +| dur | INT | Duration of startup. | +| upid | INT | Upid of process involved in startup. | +| utid | INT | Utid of the thread. | +| thread_name | STRING | Name of the thread. | +| is_main_thread | BOOL | Thread is a main thread. | + +
+ +
+ +
+ +**android_thread_slices_for_all_startups**. All the slices for all startups in trace. Generally, this view should not be used + +
+ +VIEW +All the slices for all startups in trace. + +Generally, this view should not be used. Instead, use one of the view functions related +to the startup slices which are created from this table. + +| Column | Type | Description | +|---|---|---| +| startup_ts | INT | Timestamp of startup. | +| startup_ts_end | INT | Timestamp of startup end. | +| startup_id | INT | Startup id. | +| utid | INT | UTID of thread with slice. | +| thread_name | STRING | Name of thread. | +| is_main_thread | BOOL | Whether it is main thread. | +| arg_set_id | INT | Arg set id. | +| slice_id | INT | Slice id. | +| slice_name | STRING | Name of slice. | +| slice_ts | INT | Timestamp of slice start. | +| slice_dur | INT | Slice duration. | + +
+ +
+ +#### Functions + +
+ +**android_sum_dur_for_startup_and_slice** -\> INT. Returns duration of startup for slice name. Sums duration of all slices of startup with provided name. + +
+ +
+ +
+ +Returns duration of startup for slice name. + +Sums duration of all slices of startup with provided name. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +
+ +**android_sum_dur_on_main_thread_for_startup_and_slice** -\> INT. Returns duration of startup for slice name on main thread. Sums duration of all slices of startup with provided name only on main thread. + +
+ +
+ +
+ +Returns duration of startup for slice name on main thread. + +Sums duration of all slices of startup with provided name only on main thread. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +#### Table Functions + +
+ +**android_slices_for_startup_and_slice_name** . Given a startup id and GLOB for a slice name, returns matching slices with data. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| slice_name | STRING | Glob of the slice. | + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the slice. | +| slice_name | STRING | Name of the slice. | +| slice_ts | INT | Timestamp of start of the slice. | +| slice_dur | INT | Duration of the slice. | +| thread_name | STRING | Name of the thread with the slice. | +| arg_set_id | INT | Arg set id. | + +
+ +
+ +
+ +**android_binder_transaction_slices_for_startup** . Returns binder transaction slices for a given startup id with duration over threshold. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| threshold | DOUBLE | Only return slices with duration over threshold. | + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| slice_dur | INT | Slice duration. | +| thread_name | STRING | Name of the thread with slice. | +| process | STRING | Name of the process with slice. | +| arg_set_id | INT | Arg set id. | +| is_main_thread | BOOL | Whether is main thread. | + +
+ +
+ +### android.startup.time_to_display + +#### Views/Tables + +
+ +**android_startup_time_to_display**. Startup metric defintions, which focus on the observable time range: TTID - Time To Initial Display \* https://developer.android.com/topic/performance/vitals/launch-time#time-initial \* end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display \* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD \* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. + +
+ +TABLE +Startup metric defintions, which focus on the observable time range: +TTID - Time To Initial Display +\* https://developer.android.com/topic/performance/vitals/launch-time#time-initial +\* end of first RenderThread.DrawFrame - bindApplication +TTFD - Time To Full Display +\* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD +\* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication +Googlers: see go/android-performance-metrics-glossary for details. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| time_to_initial_display | INT | Time to initial display (TTID) | +| time_to_full_display | INT | Time to full display (TTFD) | +| ttid_frame_id | INT | `android_frames.frame_id` of frame for initial display | +| ttfd_frame_id | INT | `android_frames.frame_id` of frame for full display | +| upid | INT | `process.upid` of the startup | + +
+ +
+ +### android.statsd + +#### Views/Tables + +
+ +**android_statsd_atoms**. Statsd atoms. A subset of the slice table containing statsd atom instant events. + +
+ +VIEW +Statsd atoms. + +A subset of the slice table containing statsd atom instant events. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | INT | The timestamp at the start of the slice (in nanoseconds). | +| dur | INT | The duration of the slice (in nanoseconds). | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_instruction_count | INT | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | INT | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | INT | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | INT | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| thread_ts | INT | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | INT | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | + +
+ +
+ +### android.suspend + +#### Views/Tables + +
+ +**android_suspend_state**. Table of suspended and awake slices. Selects either the minimal or full ftrace source depending on what's available, marks suspended periods, and complements them to give awake periods. + +
+ +TABLE +Table of suspended and awake slices. + +Selects either the minimal or full ftrace source depending on what's +available, marks suspended periods, and complements them to give awake +periods. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| power_state | STRING | 'awake' or 'suspended' | + +
+ +
+ +### android.winscope.inputmethod + +#### Views/Tables + +
+ +**android_inputmethod_clients**. Android inputmethod clients state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_manager_service**. Android inputmethod manager service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_service**. Android inputmethod service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.viewcapture + +#### Views/Tables + +
+ +**android_viewcapture**. Android viewcapture (from android.viewcapture data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.windowmanager + +#### Views/Tables + +
+ +**android_windowmanager**. Android WindowManager (from android.windowmanager data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +## Package: chrome + +### chrome.chrome_scrolls + +#### Views/Tables + +
+ +**chrome_scrolls**. Defines slices for all of the individual scrolls in a trace based on the LatencyInfo-based scroll definition. NOTE: this view of top level scrolls is based on the LatencyInfo definition of a scroll, which differs subtly from the definition based on EventLatencies. TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ WebView instances + +
+ +TABLE +Defines slices for all of the individual scrolls in a trace based on the +LatencyInfo-based scroll definition. + +NOTE: this view of top level scrolls is based on the LatencyInfo definition +of a scroll, which differs subtly from the definition based on +EventLatencies. +TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ +WebView instances. Currently gesture_scroll_id unique within an instance, but +is not unique across multiple instances. Switching to an EventLatency based +definition of scrolls should resolve this. + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the scroll. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| gesture_scroll_begin_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | +| gesture_scroll_end_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | + +
+ +
+ +### chrome.cpu_powerups + +#### Views/Tables + +
+ +**chrome_cpu_power_slice**. The CPU power transitions in the trace. Power states are encoded as non-negative integers, with zero representing full-power operation and positive values representing increasingly deep sleep states. On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep state that the CPU enters while idle. + +
+ +VIEW +The CPU power transitions in the trace. +Power states are encoded as non-negative integers, with zero representing +full-power operation and positive values representing increasingly deep +sleep states. + +On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep +state that the CPU enters while idle. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The CPU on which the transition occurred | +| power_state | INT | The power state that the CPU was in at time 'ts' for duration 'dur'. | +| previous_power_state | INT | The power state that the CPU was previously in. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_first_sched_slice_after_powerup**. The Linux scheduler slices that executed immediately after a CPU power up. + +
+ +TABLE +The Linux scheduler slices that executed immediately after a +CPU power up. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The cpu on which the slice executed. | +| sched_id | INT | Id for the sched_slice table. | +| utid | INT | Unique id for the thread that ran within the slice. | +| previous_power_state | INT | The CPU's power state before this slice. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_post_powerup_slice**. A table holding the slices that executed within the scheduler slice that ran on a CPU immediately after power-up. + +
+ +TABLE +A table holding the slices that executed within the scheduler +slice that ran on a CPU immediately after power-up. + +| Column | Type | Description | +|---|---|---| +| ts | None | Timestamp of the resulting slice | +| dur | None | Duration of the slice. | +| cpu | None | The CPU the sched slice ran on. | +| utid | None | Unique thread id for the slice. | +| sched_id | None | 'id' field from the sched_slice table. | +| type | None | From the sched_slice table, always 'sched_slice'. | +| end_state | None | The ending state for the sched_slice | +| priority | None | The kernel thread priority | +| slice_id | None | Id of the top-level slice for this (sched) slice. | + +
+ +
+ +
+ +**chrome_cpu_power_first_toplevel_slice_after_powerup**. The first top-level slice that ran after a CPU power-up. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | ID of the slice in the slice table. | +| previous_power_state | INT | The power state of the CPU prior to power-up. | + +
+ +
+ +### chrome.event_latency + +#### Views/Tables + +
+ +**chrome_event_latencies**. All EventLatency slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| is_presented | BOOL | Whether this input event was presented. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +
+ +**chrome_gesture_scroll_events**. All scroll-related events (frames) including gesture scroll updates, begins and ends with respective scroll ids and start/end timestamps, regardless of being presented + +
+ +TABLE +All scroll-related events (frames) including gesture scroll updates, begins +and ends with respective scroll ids and start/end timestamps, regardless of +being presented. This includes pinches that were presented. See b/315761896 +for context on pinches. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +#### Functions + +
+ +**chrome_get_most_recent_scroll_begin_id** -\> INT. Extracts scroll id for the EventLatency slice at `ts`. + +
+ +
+ +
+ +Returns INT: The event_latency_id of the EventLatency slice with the type GESTURE_SCROLL_BEGIN that is the closest to `ts`. + +| Argument | Type | Description | +|---|---|---| +| ts | INT | Timestamp of the EventLatency slice to get the scroll id for. | + +
+ +
+ +### chrome.event_latency_description + +#### Views/Tables + +
+ +**chrome_event_latency_stage_descriptions**. Source of truth of the descriptions of EventLatency stages. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | A description of the EventLatency stage. | + +
+ +
+ +### chrome.histograms + +#### Views/Tables + +
+ +**chrome_histograms**. A helper view on top of the histogram events emitted by Chrome. Requires "disabled-by-default-histogram_samples" Chrome category. + +
+ +TABLE +A helper view on top of the histogram events emitted by Chrome. +Requires "disabled-by-default-histogram_samples" Chrome category. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the histogram. | +| value | INT | The value of the histogram sample. | +| ts | INT | Alias of \|slice.ts\|. | +| thread_name | STRING | Thread name. | +| utid | INT | Utid of the thread. | +| tid | INT | Tid of the thread. | +| process_name | STRING | Process name. | +| upid | INT | Upid of the process. | +| pid | INT | Pid of the process. | + +
+ +
+ +### chrome.interactions + +#### Views/Tables + +
+ +**chrome_interactions**. All critical user interaction events, including type and table with associated metrics. + +
+ +TABLE +All critical user interaction events, including type and table with +associated metrics. + +| Column | Type | Description | +|---|---|---| +| scoped_id | INT | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | +| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | +| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | +| ts | INT | Timestamp of the CUI event. | +| dur | INT | Duration of the CUI event. | + +
+ +
+ +### chrome.metadata + +#### Functions + +
+ +**chrome_hardware_class** -\> STRING. Returns hardware class of the device, often use to find device brand and model. + +
+ +
+ +
+ +Returns hardware class of the device, often use to find device brand +and model. +Returns STRING: Hardware class name. + +
+ +
+ +### chrome.page_loads + +#### Views/Tables + +
+ +**chrome_page_loads**. Chrome page loads, including associated high-level metrics and properties. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | +| navigation_id | INT | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | +| navigation_start_ts | INT | Timestamp of the start of navigation. | +| fcp | INT | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | +| fcp_ts | INT | Timestamp of the first contentful paint. | +| lcp | INT | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | +| lcp_ts | INT | Timestamp of the largest contentful paint. | +| dom_content_loaded_event_ts | INT | Timestamp of the DomContentLoaded event: https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event | +| load_event_ts | INT | Timestamp of the window load event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event | +| mark_fully_loaded_ts | INT | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | +| mark_fully_visible_ts | INT | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | +| mark_interactive_ts | INT | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | +| url | STRING | URL at the page load event. | +| browser_upid | INT | The unique process id (upid) of the browser process where the page load occurred. | + +
+ +
+ +### chrome.scroll_interactions + +#### Views/Tables + +
+ +**chrome_scroll_interactions**. Top level scroll events, with metrics. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for an individual scroll. | +| name | STRING | Name of the scroll event. | +| ts | INT | Start timestamp of the scroll. | +| dur | INT | Duration of the scroll. | +| frame_count | INT | The total number of frames in the scroll. | +| vsync_count | INT | The total number of vsyncs in the scroll. | +| missed_vsync_max | INT | The maximum number of vsyncs missed during any and all janks. | +| missed_vsync_sum | INT | The total number of vsyncs missed during any and all janks. | +| delayed_frame_count | INT | The number of delayed frames. | +| predictor_janky_frame_count | INT | The number of frames that are deemed janky to the human eye after Chrome has applied its scroll prediction algorithm. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +### chrome.scroll_jank.predictor_error + +#### Views/Tables + +
+ +**chrome_predictor_error**. The scrolling offsets and predictor jank values for the actual (applied) scroll events. + +
+ +TABLE +The scrolling offsets and predictor jank values for the actual (applied) +scroll events. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| present_ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | +| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | +| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | +| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | +| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | +| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_map + +#### Views/Tables + +
+ +**chrome_scroll_jank_cause_descriptions**. Source of truth of the descriptions of EventLatency-based scroll jank causes. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| event_latency_stage | STRING | The name of the EventLatency stage. | +| cause_process | STRING | The process where the cause of scroll jank occurred. | +| cause_thread | STRING | The thread where the cause of scroll jank occurred. | +| cause_description | STRING | A description of the cause of scroll jank. | + +
+ +
+ +
+ +**chrome_scroll_jank_causes_with_event_latencies**. Combined description of scroll jank cause and associated event latency stage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | Description of the EventLatency stage. | +| cause_process | STRING | The process name that may cause scroll jank. | +| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | +| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_utils + +#### Table Functions + +
+ +**chrome_select_scroll_jank_cause_thread** . Function to retrieve the thread id of the thread on a particular process if there are any slices during a particular EventLatency slice duration; this upid/thread combination refers to a cause of Scroll Jank. + +
+ +
+ +
+ +Function to retrieve the thread id of the thread on a particular process if +there are any slices during a particular EventLatency slice duration; this +upid/thread combination refers to a cause of Scroll Jank. +Argument \| Type \| Description +--- \| --- \| --- +event_latency_id \| INT \| The slice id of an EventLatency slice. +process_type \| STRING \| The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. +thread_name \| STRING \| The name of the thread. + +| Column | Type | Description | +|---|---|---| +| utid | INT | The utid associated with | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_intervals + +#### Views/Tables + +
+ +**chrome_janky_event_latencies_v3**. Selects EventLatency slices that correspond with janks in a scroll + +
+ +TABLE +Selects EventLatency slices that correspond with janks in a scroll. This is +based on the V3 version of scroll jank metrics. + +| Column | Type | Description | +|---|---|---| +| id | INT | The slice id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| track_id | INT | The track_id for the slice. | +| name | STRING | The name of the slice (EventLatency). | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| frame_jank_ts | INT | The start timestamp where frame presentation was delayed. | +| frame_jank_dur | INT | The duration in ms of the delay in frame presentation. | + +
+ +
+ +
+ +**chrome_janky_frame_presentation_intervals**. Frame presentation interval is the delta between when the frame was supposed to be presented and when it was actually presented. + +
+ +VIEW +Frame presentation interval is the delta between when the frame was supposed +to be presented and when it was actually presented. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| event_latency_id | INT | The id of the associated event latency in the slice table. | + +
+ +
+ +
+ +**chrome_scroll_stats**. Scroll jank frame presentation stats for individual scrolls. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | Id of the individual scroll. | +| frame_count | INT | The number of frames in the scroll. | +| missed_vsyncs | INT | The number of missed vsyncs in the scroll. | +| presented_frame_count | INT | The number presented frames in the scroll. | +| janky_frame_count | INT | The number of janky frames in the scroll. | +| janky_frame_percent | FLOAT | The % of frames that janked in the scroll. | + +
+ +
+ +
+ +**chrome_scroll_jank_intervals_v3**. Defines slices for all of janky scrolling intervals in a trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the janky interval. | +| ts | INT | The start timestamp of the janky interval. | +| dur | INT | The duration of the janky interval. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3 + +#### Views/Tables + +
+ +**chrome_gesture_scroll_updates**. Grabs all gesture updates with respective scroll ids and start/end timestamps, regardless of being presented. + +
+ +TABLE +Grabs all gesture updates with respective scroll ids and start/end +timestamps, regardless of being presented. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| id | INT | Slice id for the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_presented_gesture_scrolls**. Scroll updates, corresponding to all input events that were converted to a presented scroll update. + +
+ +TABLE +Scroll updates, corresponding to all input events that were converted to a +presented scroll update. + +| Column | Type | Description | +|---|---|---| +| id | INT | Minimum slice id for input presented in this frame, the non-presented input. | +| ts | INT | The start timestamp for producing the frame. | +| dur | INT | The duration between producing and presenting the frame. | +| last_presented_input_ts | INT | The timestamp of the last input that arrived and got presented in the frame. | +| scroll_update_id | INT | The id of the scroll update event, a unique identifier to the gesture. | +| scroll_id | INT | The id of the ongoing scroll. | +| presentation_timestamp | INT | Frame presentation timestamp. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_scroll_updates_with_deltas**. Associate every trace_id with it's perceived delta_y on the screen after prediction. + +
+ +TABLE +Associate every trace_id with it's perceived delta_y on the screen after +prediction. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | INT | The id of the scroll update event. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | + +
+ +
+ +
+ +**chrome_full_frame_view**. Obtain the subset of input events that were fully presented. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_full_frame_delta_view**. Join deltas with EventLatency data. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_merged_frame_view**. Group all gestures presented at the same timestamp together in a single row. + +
+ +TABLE +Group all gestures presented at the same timestamp together in +a single row. + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| max_start_ts | INT | The timestamp of the last presented input. | +| min_start_ts | INT | The earliest frame start timestamp. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | +| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | +| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Maximum duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_frame_info_with_delay**. View contains all chrome presented frames during gesture updates while calculating delay since last presented which usually should equal to \|VSYNC_INTERVAL\| if no jank is present. + +
+ +TABLE +View contains all chrome presented frames during gesture updates +while calculating delay since last presented which usually should +equal to \|VSYNC_INTERVAL\| if no jank is present. + +| Column | Type | Description | +|---|---|---| +| id | INT | gesture scroll slice id. | +| max_start_ts | INT | OS timestamp of the last touch move arrival within a frame. | +| min_start_ts | INT | OS timestamp of the first touch move arrival within a frame. | +| scroll_id | INT | The scroll which the touch belongs to. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | +| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | +| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives \|total_delta\|. | +| event_latency_id | INT | Event latency id of the presented frame. | +| dur | INT | Duration of the EventLatency. | +| presentation_timestamp | INT | Timestamp at which the frame was shown on the screen. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals \|VSYNC\| if no frame drops happened. | +| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_vsyncs**. Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the minimum delay between frames larger than zero. TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +
+ +TABLE +Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the +minimum delay between frames larger than zero. + +TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +| Column | Type | Description | +|---|---|---| +| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | + +
+ +
+ +
+ +**chrome_janky_frames_no_cause**. Filter the frame view only to frames that had missed vsyncs. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_janky_frames_no_subcause**. Janky frame information including the jank cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | +| cause_id | INT | Id of the slice corresponding to the offending stage. | + +
+ +
+ +
+ +**chrome_janky_frames**. Finds all causes of jank for all janky frames, and a cause of sub jank if the cause of jank was GPU related. + +
+ +TABLE +Finds all causes of jank for all janky frames, and a cause of sub jank +if the cause of jank was GPU related. + +| Column | Type | Description | +|---|---|---| +| cause_of_jank | STRING | The reason the Vsync was missed. | +| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | + +
+ +
+ +
+ +**chrome_unique_frame_presentation_ts**. Counting all unique frame presentation timestamps. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| presentation_timestamp | INT | The unique frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_janky_frames_percentage**. Dividing missed frames over total frames to get janky frame percentage. This represents the v3 scroll jank metrics. Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +
+ +TABLE +Dividing missed frames over total frames to get janky frame percentage. +This represents the v3 scroll jank metrics. +Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +| Column | Type | Description | +|---|---|---| +| delayed_frame_percentage | FLOAT | The percent of missed frames relative to total frames - aka the percent of janky frames. | + +
+ +
+ +
+ +**chrome_frames_per_scroll**. Number of frames and janky frames per scroll. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| num_frames | INT | The number of frames in the scroll. | +| num_janky_frames | INT | The number of delayed/janky frames. | +| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | + +
+ +
+ +
+ +**chrome_causes_per_scroll**. Scroll jank causes per scroll. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| max_delay_since_last_frame | DOUBLE | The maximum time a frame was delayed after the presentation of the previous frame. | +| vsync_interval | DOUBLE | The expected vsync interval. | +| scroll_jank_causes | BYTES | A proto amalgamation of each scroll jank cause including cause name, sub cause and the duration of the delay since the previous frame was presented. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3_cause + +#### Functions + +
+ +**chrome_get_v3_jank_cause_id** -\> LONG. Given two slice Ids A and B, find the maximum difference between the durations of it's direct children with matching names for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) and slice B has children named (X, Y) with durations of (9, 9), the function will return the slice id of the slice named Z that is A's child, as no matching slice named Z was found under B, making 5 - 0 = 5 the maximum delta between both slice's direct children + +
+ +
+ +
+ +Given two slice Ids A and B, find the maximum difference +between the durations of it's direct children with matching names +for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) +and slice B has children named (X, Y) with durations of (9, 9), the function will return +the slice id of the slice named Z that is A's child, as no matching slice named Z was found +under B, making 5 - 0 = 5 the maximum delta between both slice's direct children +Returns LONG: The slice id of the breakdown that has the maximum duration delta. + +| Argument | Type | Description | +|---|---|---| +| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | +| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to \|janky_slice_id\|. | + +
+ +
+ +### chrome.scroll_jank.scroll_offsets + +#### Views/Tables + +
+ +**chrome_scroll_input_offsets**. The raw coordinates and pixel offsets for all input events which were part of a scroll. + +
+ +TABLE +The raw coordinates and pixel offsets for all input events which were part of +a scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Timestamp the of the scroll input event. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +
+ +**chrome_presented_scroll_offsets**. The scrolling offsets for the actual (applied) scroll events + +
+ +TABLE +The scrolling offsets for the actual (applied) scroll events. These are not +necessarily inclusive of all user scroll events, rather those scroll events +that are actually processed. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +### chrome.scroll_jank.utils + +#### Table Functions + +
+ +**chrome_select_long_task_slices** . Extract mojo information for the long-task-tracking scenario for specific names + +
+ +
+ +
+ +Extract mojo information for the long-task-tracking scenario for specific +names. For example, LongTaskTracker slices may have associated IPC +metadata, or InterestingTask slices for input may have associated IPC to +determine whether the task is fling/etc. +Argument \| Type \| Description +--- \| --- \| --- +name \| STRING \| The name of slice. + +| Column | Type | Description | +|---|---|---| +| interface_name | STRING | Name of the interface of the IPC call. | +| ipc_hash | INT | Hash of the IPC call. | +| message_type | STRING | Message type (e.g. reply). | +| id | INT | The slice id. | + +
+ +
+ +### chrome.speedometer + +#### Views/Tables + +
+ +**chrome_speedometer_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_iteration**. Slice that covers one Speedometer iteration. Depending on the Speedometer version these slices might need to be estimated as older versions of Speedometer to not emit marks for this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +Depending on the Speedometer version these slices might need to be estimated +as older versions of Speedometer to not emit marks for this interval. The +metrics associated are the same ones Speedometer would output, but note we +use ns precision (Speedometer uses \~100us) so the actual values might differ +a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_score** -\> DOUBLE. Returns the Speedometer score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer score + +
+ +
+ +
+ +**chrome_speedometer_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_2_1 + +#### Views/Tables + +
+ +**chrome_speedometer_2_1_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async sync is the time between the start and sync-end marks, async is the time between the sync-end and async-end marks. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async +sync is the time between the start and sync-end marks, async is the time +between the sync-end and async-end marks. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_2_1_iteration**. Slice that covers one Speedometer iteration. This slice is actually estimated as a default Speedometer run will not emit marks to cover this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +This slice is actually estimated as a default Speedometer run will not emit +marks to cover this interval. The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. Also note Speedometer +returns the values in ms these here and in ns. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_2_1_score** -\> DOUBLE. Returns the Speedometer 2.1 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 2.1 score + +
+ +
+ +
+ +**chrome_speedometer_2_1_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 2.1 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_3 + +#### Views/Tables + +
+ +**chrome_speedometer_3_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_3_iteration**. Slice that covers one Speedometer iteration. The metrics associated are the same ones Speedometer would output, but note we use ns precision (Speedometer uses \~100us) so the actual values might differ a bit. + +
+ +TABLE +Slice that covers one Speedometer iteration. +The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_3_score** -\> DOUBLE. Returns the Speedometer 3 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 3 score + +
+ +
+ +
+ +**chrome_speedometer_3_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.startups + +#### Views/Tables + +
+ +**chrome_startups**. Chrome startups, including launch cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique ID | +| activity_id | INT | Chrome Activity event id of the launch. | +| name | STRING | Name of the launch start event. | +| startup_begin_ts | INT | Timestamp that the startup occurred. | +| first_visible_content_ts | INT | Timestamp to the first visible content. | +| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | +| browser_upid | INT | Process ID of the Browser where the startup occurred. | + +
+ +
+ +### chrome.tasks + +#### Views/Tables + +
+ +**chrome_java_views**. A list of slices corresponding to operations on interesting (non-generic) Chrome Java views + +
+ +VIEW +A list of slices corresponding to operations on interesting (non-generic) +Chrome Java views. The view is considered interested if it's not a system +(ContentFrameLayout) or generic library (CompositorViewHolder) views. + +TODO(altimin): Add "columns_from slice" annotation. +TODO(altimin): convert this to EXTEND_TABLE when it becomes available. + +| Column | Type | Description | +|---|---|---| +| filtered_name | STRING | Name of the view. | +| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | +| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | +| slice_id | INT | Slice id. | + +
+ +
+ +
+ +**chrome_scheduler_tasks**. A list of tasks executed by Chrome scheduler. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| type | STRING | Type. | +| name | STRING | Name of the task. | +| ts | INT | Timestamp. | +| dur | INT | Duration. | +| utid | INT | Utid of the thread this task run on. | +| thread_name | STRING | Name of the thread this task run on. | +| upid | INT | Upid of the process of this task. | +| process_name | STRING | Name of the process of this task. | +| track_id | INT | Same as slice.track_id. | +| category | STRING | Same as slice.category. | +| depth | INT | Same as slice.depth. | +| parent_id | INT | Same as slice.parent_id. | +| arg_set_id | INT | Same as slice.arg_set_id. | +| thread_ts | INT | Same as slice.thread_ts. | +| thread_dur | INT | Same as slice.thread_dur. | +| posted_from | STRING | Source location where the PostTask was called. | + +
+ +
+ +
+ +**chrome_tasks**. A list of "Chrome tasks": top-level execution units (e.g + +
+ +VIEW +A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / +IPCs / system callbacks) run by Chrome. For a given thread, the slices +corresponding to these tasks will not intersect. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id for the given task, also the id of the slice this task corresponds to. | +| name | STRING | Name for the given task. | +| task_type | STRING | Type of the task (e.g. "scheduler"). | +| thread_name | STRING | Thread name. | +| utid | INT | Utid. | +| process_name | STRING | Process name. | +| upid | INT | Upid. | +| ts | INT | Alias of \|slice.ts\|. | +| dur | INT | Alias of \|slice.dur\|. | +| track_id | INT | Alias of \|slice.track_id\|. | +| category | STRING | Alias of \|slice.category\|. | +| arg_set_id | INT | Alias of \|slice.arg_set_id\|. | +| thread_ts | INT | Alias of \|slice.thread_ts\|. | +| thread_dur | INT | Alias of \|slice.thread_dur\|. | +| full_name | STRING | STRING Legacy alias for \|name\|. | + +
+ +
+ +### chrome.vsync_intervals + +#### Views/Tables + +
+ +**chrome_vsync_intervals**. A simple table that checks the time between VSync (this can be used to determine if we're refreshing at 90 FPS or 60 FPS). Note: In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +
+ +TABLE +A simple table that checks the time between VSync (this can be used to +determine if we're refreshing at 90 FPS or 60 FPS). + +> [!NOTE] +> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Slice id of the vsync slice. | +| ts | INT | Timestamp of the vsync slice. | +| dur | INT | Duration of the vsync slice. | +| track_id | INT | Track id of the vsync slice. | +| time_to_next_vsync | INT | Duration until next vsync arrives. | + +
+ +
+ +#### Functions + +
+ +**chrome_calculate_avg_vsync_interval** -\> FLOAT. Function: compute the average Vysnc interval of the gesture (hopefully this would be either 60 FPS for the whole gesture or 90 FPS but that isnt always the case) on the given time segment. If the trace doesnt contain the VSync TraceEvent we just fall back on assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which corresponds to 16 ms or 60 FPS). + +
+ +
+ +
+ +Function: compute the average Vysnc interval of the +gesture (hopefully this would be either 60 FPS for the whole gesture or 90 +FPS but that isnt always the case) on the given time segment. +If the trace doesnt contain the VSync TraceEvent we just fall back on +assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which +corresponds to 16 ms or 60 FPS). +Returns FLOAT: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. + +| Argument | Type | Description | +|---|---|---| +| begin_ts | LONG | Interval start time. | +| end_ts | LONG | Interval end time. | + +
+ +
+ +### chrome.web_content_interactions + +#### Views/Tables + +
+ +**chrome_web_content_interactions**. Chrome web content interactions (InteractionToFirstPaint), including associated high-level metrics and properties. Multiple events may occur for the same interaction; each row in this table represents the primary (longest) event for the interaction. Web content interactions are discrete, as opposed to sustained (e.g. scrolling); and only occur with the web content itself, as opposed to other parts of Chrome (e.g + +
+ +TABLE +Chrome web content interactions (InteractionToFirstPaint), including +associated high-level metrics and properties. + +Multiple events may occur for the same interaction; each row in this table +represents the primary (longest) event for the interaction. + +Web content interactions are discrete, as opposed to sustained (e.g. +scrolling); and only occur with the web content itself, as opposed to other +parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, +keyboard input (typing), and drags. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for this interaction. | +| ts | INT | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | +| dur | INT | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | +| interaction_type | STRING | The interaction type. | +| total_duration_ms | INT | The total duration of all events that occurred for the same interaction. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +## Package: counters + +### counters.intervals + +#### Macros + +
+ +**counter_leading_intervals** . For a given counter timeline (e.g + +
+ +
+ +
+ +For a given counter timeline (e.g. a single counter track), returns +intervals of time where the counter has the same value. + +Intervals are computed in a "forward-looking" way. That is, if a counter +changes value at some timestamp, it's assumed it *just* reached that +value and it should continue to have that value until the next +value change. The final value is assumed to hold until the very end of +the trace. + +For example, suppose we have the following data: +`ts=0, value=10, track_id=1 +ts=0, value=10, track_id=2 +ts=10, value=10, track_id=1 +ts=10, value=20, track_id=2 +ts=20, value=30, track_id=1 +[end of trace at ts = 40]` + +Then this macro will generate the following intervals: +`ts=0, dur=20, value=10, track_id=1 +ts=20, dur=10, value=30, track_id=1 +ts=0, dur=10, value=10, track_id=2 +ts=10, dur=30, value=20, track_id=2` +Returns: TableOrSubquery, Table with the schema (id UINT32, ts UINT64, dur UINT64, track_id UINT64, value DOUBLE, next_value DOUBLE, delta_value DOUBLE). + +| Argument | Type | Description | +|---|---|---| +| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | + +
+ +
+ +## Package: graphs + +### graphs.dominator_tree + +#### Macros + +
+ +**graph_dominator_tree** . Given a table containing a directed flow-graph and an entry node, computes the "dominator tree" for the graph + +
+ +
+ +
+ +Given a table containing a directed flow-graph and an entry node, computes +the "dominator tree" for the graph. See \[1\] for an explanation of what a +dominator tree is. + +\[1\] https://en.wikipedia.org/wiki/Dominator_(graph_theory) + +Example usage on traces containing heap graphs: +\`\`\` +CREATE PERFETTO VIEW dominator_compatible_heap_graph AS +-- Extract the edges from the heap graph which correspond to references +-- between objects. +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +JOIN heap_graph_object owner on heap_graph_reference.owner_id = owner.id +WHERE owned_id IS NOT NULL AND owner.reachable +UNION ALL +-- Since a Java heap graph is a "forest" structure, we need to add a dummy +-- "root" node which connects all the roots of the forest into a single +-- connected component. +SELECT +(SELECT max(id) + 1 FROM heap_graph_object) as source_node_id, +id +FROM heap_graph_object +WHERE root_type IS NOT NULL; + +SELECT \* +FROM graph_dominator_tree!( +dominator_compatible_heap_graph, +(SELECT max(id) + 1 FROM heap_graph_object) +); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, dominator_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with \|root_node_id\| (see below) being the "entry node" for this component. Specifically, all nodes *must* be reachable by following paths from the root node. Failing to adhere to this property will result in undefined behaviour. If working with a "forest"-like structure, a dummy node should be added which links all the roots of the forest together into a single component; an example of this can be found in the heap graph example query above. | +| root_node_id | Expr | The entry node to \|graph_table\| which will be the root of the dominator tree. | + +
+ +
+ +### graphs.partition + +#### Macros + +
+ +**tree_structural_partition_by_group** . Partitions a tree into a forest of trees based on a given grouping key in a structure-preserving way. Specifically, for each tree in the output forest, all the nodes in that tree have the same ancestors and descendants as in the original tree *iff* that ancestor/descendent belonged to the same group. Example: Input id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 4 \| 1 Or as a graph: `1 (1) / 2 (1) / \ 3 (2) 4 (2) / \ 5 (1) 8 (1) / \ 6 (3) 7 (2)` Possible output (order of rows is implementation-defined) id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 2 \| 1 Or as a forest: `1 (1) 3 (2) 4 (2) 6 (3) | | 2 (1) 7 (2) / \ 5 (1) 8 (1)` + +
+ +
+ +
+ +Partitions a tree into a forest of trees based on a given grouping key +in a structure-preserving way. + +Specifically, for each tree in the output forest, all the nodes in that tree +have the same ancestors and descendants as in the original tree *iff* that +ancestor/descendent belonged to the same group. + +Example: +Input + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 4 | 1 | + +Or as a graph: +`1 (1) +/ +2 (1) +/ \ +3 (2) 4 (2) +/ \ +5 (1) 8 (1) +/ \ +6 (3) 7 (2)` +Possible output (order of rows is implementation-defined) + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 2 | 1 | + +Or as a forest: +`1 (1) 3 (2) 4 (2) 6 (3) +| | +2 (1) 7 (2) +/ \ +5 (1) 8 (1)` +Returns: TableOrSubquery, The returned table has the schema (id UINT32, parent_id UINT32, group_key UINT32). + +| Argument | Type | Description | +|---|---|---| +| tree_table | TableOrSubquery | A table/view/subquery corresponding to a tree which should be partitioned. This table must have the columns "id", "parent_id" and "group_key". Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | + +
+ +
+ +### graphs.search + +#### Macros + +
+ +**graph_reachable_dfs** . Computes the "reachable" set of nodes in a directed graph from a given set of starting nodes by performing a depth-first search on the graph + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a given set +of starting nodes by performing a depth-first search on the graph. The +returned nodes are structured as a tree with parent-child relationships +corresponding to the order in which nodes were encountered by the DFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: +`-- Compute the reachable nodes from the first heap root. +SELECT * +FROM graph_reachable_dfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +
+ +
+ +
+ +**graph_reachable_bfs** . Computes the "reachable" set of nodes in a directed graph from a given starting node by performing a breadth-first search on the graph + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a given +starting node by performing a breadth-first search on the graph. The returned +nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the BFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: +`-- Compute the reachable nodes from all heap roots. +SELECT * +FROM graph_reachable_bfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +
+ +
+ +
+ +**graph_next_sibling** . Computes the next sibling node in a directed graph + +
+ +
+ +
+ +Computes the next sibling node in a directed graph. The next node under a parent node +is determined by on the \|sort_key\|, which should be unique for every node under a parent. +The order of the next sibling is undefined if the \|sort_key\| is not unique. + +Example usage: +`-- Compute the next sibling: +SELECT * +FROM graph_next_sibling!( +( +SELECT +id AS node_id, +parent_id AS node_parent_id, +ts AS sort_key +FROM slice +) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, next_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph for which to find the next sibling. This table must have the columns "node_id", "node_parent_id" and "sort_key". | + +
+ +
+ +
+ +**graph_reachable_weight_bounded_dfs** . Computes the "reachable" set of nodes in a directed graph from a set of starting (root) nodes by performing a depth-first search from each root node on the graph. The search is bounded by the sum of edge weights on the path and the root node specifies the max weight (inclusive) allowed before stopping the search. The returned nodes are structured as a tree with parent-child relationships corresponding to the order in which nodes were encountered by the DFS + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a set of +starting (root) nodes by performing a depth-first search from each root node on the graph. +The search is bounded by the sum of edge weights on the path and the root node specifies the +max weight (inclusive) allowed before stopping the search. +The returned nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the DFS. Each row also has the root node from +which where the edge was encountered. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces with sched info: +\`\`\` +-- Compute the reachable nodes from a sched wakeup chain +INCLUDE PERFETTO MODULE sched.thread_executing_spans; + +SELECT \* +FROM +graph_reachable_dfs_bounded +!( +( +SELECT +id AS source_node_id, +COALESCE(parent_id, id) AS dest_node_id, +id - COALESCE(parent_id, id) AS edge_weight +FROM _wakeup_chain +), +( +SELECT +id AS root_node_id, +id - COALESCE(prev_id, id) AS root_target_weight +FROM _wakeup_chain +)); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph and an "edge_weight" corresponding to the weight of the edge between the node. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to \|graph_table\| which will be the roots of the reachability trees. This table must have the columns "root_node_id" and "root_target_weight" corresponding to the starting node id and the max weight allowed on the tree. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| is_target_weight_floor | Expr | Whether the target_weight is a floor weight or ceiling weight. If it's floor, the search stops right after we exceed the target weight, and we include the node that pushed just passed the target. If ceiling, the search stops right before the target weight and the node that would have pushed us passed the target is not included. | + +
+ +
+ +## Package: export + +### export.to_firefox_profile + +#### Functions + +
+ +**export_to_firefox_profile** -\> STRING. Dumps all trace data as a Firefox profile json string See `Profile` in https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js Also https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html You would probably want to download the generated json and then open at https://https://profiler.firefox.com You can easily do this from the UI via the following SQL `SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` The result will have a link for you to download this json as a file. + +
+ +
+ +
+ +Dumps all trace data as a Firefox profile json string +See `Profile` in +https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js +Also +https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html + +You would probably want to download the generated json and then open at +https://https://profiler.firefox.com +You can easily do this from the UI via the following SQL +`SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` +The result will have a link for you to download this json as a file. +Returns STRING: Json profile + +
+ +
\ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/perfetto-stdlib.md b/profilers/perfetto-trace-analysis/references/perfetto-stdlib.md new file mode 100644 index 0000000..a2e9c6c --- /dev/null +++ b/profilers/perfetto-trace-analysis/references/perfetto-stdlib.md @@ -0,0 +1,6670 @@ +*This page documents the PerfettoSQL standard library.* + +## Introduction + +The PerfettoSQL standard library is a repository of tables, views, functions +and macros, contributed by domain experts, which make querying traces easier +Its design is heavily inspired by standard libraries in languages like Python, +C++ and Java. + +Some of the purposes of the standard library include: +1) Acting as a way of sharing and commonly written queries without needing +to copy/paste large amounts of SQL. +2) Raising the abstraction level when exposing data in the trace. Many +modules in the standard library convert low-level trace concepts +e.g. slices, tracks and into concepts developers may be more familar with +e.g. for Android developers: app startups, binder transactions etc. + +Standard library modules can be included as follows + + -- Include all tables/views/functions from the android.startup.startups + -- module in the standard library. + INCLUDE PERFETTO MODULE android.startup.startups; + + -- Use the android_startups table defined in the android.startup.startups + -- module. + SELECT * + FROM android_startups; + +Prelude is a special module is automatically included. It contains key helper +tables, views and functions which are universally useful. + +More information on importing modules is available in the +[syntax documentation](https://perfetto.dev/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) +for the `INCLUDE PERFETTO MODULE` statement. + +## Package: prelude + +#### Views/Tables + +
+ +**trace_metrics**. Lists all metrics built-into trace processor. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the metric. | + +
+ +
+ +
+ +**track**. Tracks are a fundamental concept in trace processor and represent a "timeline" for events of the same type and with the same context + +
+ +VIEW +Tracks are a fundamental concept in trace processor and represent a +"timeline" for events of the same type and with the same context. See +https://perfetto.dev/docs/analysis/trace-processor#tracks for a more +detailed explanation, with examples. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this track. Identical to \|track_id\|, prefer using \|track_id\| instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track; can be null for some types of tracks (e.g. thread tracks). | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. Alias of `args.arg_set_id`. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | + +
+ +
+ +
+ +**cpu**. Contains information about the CPUs on the device this trace was taken on. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this CPU. Identical to \|ucpu\|, prefer using \|ucpu\| instead. | +| ucpu | UINT | Unique identifier for this CPU. Isn't equal to \|cpu\| for remote machines and is equal to \|cpu\| for the host machine. | +| cpu | UINT | The 0-based CPU core identifier. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| cluster_id | UINT | The cluster id is shared by CPUs in the same cluster. | +| processor | STRING | A string describing this core. | +| machine_id | UINT | Machine identifier, non-null for CPUs on a remote machine. | +| capacity | UINT | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt | +| arg_set_id | UINT | Extra key/value pairs associated with this cpu. | + +
+ +
+ +
+ +**cpu_available_frequencies**. Contains the frequency values that the CPUs on the device are capable of running at. + +
+ +VIEW +Contains the frequency values that the CPUs on the device are capable of +running at. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu frequency. | +| cpu | UINT | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| freq | UINT | CPU frequency in KHz. | +| ucpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | + +
+ +
+ +
+ +**sched_slice**. This table holds slices with kernel thread scheduling information + +
+ +VIEW +This table holds slices with kernel thread scheduling information. These +slices are collected when the Linux "ftrace" data source is used with the +"sched/switch" and "sched/wakeup\*" events enabled. + +The rows in this table will always have a matching row in the \|thread_state\| +table with \|thread_state.state\| = 'Running' + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | + +
+ +
+ +
+ +**sched** . Shorter alias for table `sched_slice`. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Alias for `sched_slice.id`. | +| type | STRING | Alias for `sched_slice.type`. | +| ts | LONG | Alias for `sched_slice.ts`. | +| dur | LONG | Alias for `sched_slice.dur`. | +| cpu | UINT | Alias for `sched_slice.cpu`. | +| utid | UINT | Alias for `sched_slice.utid`. | +| end_state | STRING | Alias for `sched_slice.end_state`. | +| priority | INT | Alias for `sched_slice.priority`. | +| ucpu | UINT | Alias for `sched_slice.ucpu`. | +| ts_end | UINT | Legacy column, should no longer be used. | + +
+ +
+ +
+ +**thread_state**. This table contains the scheduling state of every thread on the system during the trace. The rows in this table which have \|state\| = 'Running', will have a corresponding row in the \|sched_slice\| table. + +
+ +VIEW +This table contains the scheduling state of every thread on the system during +the trace. + +The rows in this table which have \|state\| = 'Running', will have a +corresponding row in the \|sched_slice\| table. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this thread state. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in \|sched_slice.end_state\|. | +| io_wait | UINT | Indicates whether this thread was blocked on IO. | +| blocked_function | STRING | The function in the kernel this thread was blocked on. | +| waker_utid | UINT | The unique thread id of the thread which caused a wakeup of this thread. | +| waker_id | UINT | The unique thread state id which caused a wakeup of this thread. | +| irq_context | UINT | Whether the wakeup was from interrupt context or process context. | +| ucpu | UINT | The unique CPU identifier that the thread executed on. | + +
+ +
+ +
+ +**raw**. Contains 'raw' events from the trace for some types of events + +
+ +VIEW +Contains 'raw' events from the trace for some types of events. This table +only exists for debugging purposes and should not be relied on in production +usecases (i.e. metrics, standard library etc.) + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this raw event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | + +
+ +
+ +
+ +**ftrace_event**. Contains all the ftrace events in the trace + +
+ +VIEW +Contains all the ftrace events in the trace. This table exists only for +debugging purposes and should not be relied on in production usecases (i.e. +metrics, standard library etc). Note also that this table might be empty if +raw ftrace parsing has been disabled. + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this ftrace event. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp of this event. | +| name | STRING | The ftrace event name. | +| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread this event was emitted on. | +| arg_set_id | UINT | The set of key/value pairs associated with this event. | +| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | +| ucpu | UINT | The unique CPU identifier that this event was emitted on. | + +
+ +
+ +
+ +**experimental_sched_upid**. The sched_slice table with the upid column. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this scheduling slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| utid | UINT | The thread's unique id in the trace. | +| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | +| priority | INT | The kernel priority that the thread ran at. | +| ucpu | UINT | The unique CPU identifier that the slice executed on. | +| upid | UINT | The process's unique id in the trace. | + +
+ +
+ +
+ +**cpu_track**. Tracks which are associated to a single CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**cpu_counter_track**. Tracks containing counter-like events associated to a CPU. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Unique identifier for this cpu counter track. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| name | STRING | Name of the track. | +| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | +| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | +| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | +| unit | STRING | The units of the counter. This column is rarely filled. | +| description | STRING | The description for this track. For debugging purposes only. | +| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | +| ucpu | UINT | The unique CPU identifier that this track is associated with. | + +
+ +
+ +
+ +**counters** . Alias of the `counter` table. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias of `counter.id`. | +| type | STRING | Alias of `counter.type`. | +| ts | LONG | Alias of `counter.ts`. | +| track_id | INT | Alias of `counter.track_id`. | +| value | DOUBLE | Alias of `counter.value`. | +| arg_set_id | INT | Alias of `counter.arg_set_id`. | +| name | STRING | Legacy column, should no longer be used. | +| unit | STRING | Legacy column, should no longer be used. | +| description | STRING | Legacy column, should no longer be used. | + +
+ +
+ +
+ +**slice**. Contains slices from userspace which explains what threads were doing during the trace. + +
+ +VIEW +Contains slices from userspace which explains what threads were doing +during the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | +| dur | LONG | The duration of the slice (in nanoseconds). | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | LONG | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | LONG | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_ts | LONG | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| cat | STRING | Alias of `category`. | +| slice_id | LONG | Alias of `id`. | + +
+ +
+ +
+ +**instant**. Contains instant events from userspace which indicates what happened at a single moment in time. + +
+ +VIEW +Contains instant events from userspace which indicates what happened at a +single moment in time. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | The timestamp of the instant (in nanoseconds). | +| track_id | INT | The id of the track this instant is located on. | +| name | STRING | The name of the instant. The name describes what happened during the instant. | +| arg_set_id | INT | The id of the argument set associated with this instant. | + +
+ +
+ +
+ +**slices** . Alternative alias of table `slice`. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | UINT | Alias of `slice.id`. | +| type | STRING | Alias of `slice.type`. | +| ts | LONG | Alias of `slice.ts`. | +| dur | LONG | Alias of `slice.dur`. | +| track_id | INT | Alias of `slice.track_id`. | +| category | STRING | Alias of `slice.category`. | +| name | STRING | Alias of `slice.name`. | +| depth | INT | Alias of `slice.depth`. | +| stack_id | LONG | Alias of `slice.stack_id`. | +| parent_stack_id | LONG | Alias of `slice.parent_stack_id`. | +| parent_id | INT | Alias of `slice.parent_id`. | +| arg_set_id | INT | Alias of `slice.arg_set_id`. | +| thread_ts | LONG | Alias of `slice.thread_ts`. | +| thread_dur | LONG | Alias of `slice.thread_dur`. | +| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | +| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | +| cat | LONG | Alias of `slice.cat`. | +| slice_id | LONG | Alias of `slice.slice_id`. | + +
+ +
+ +
+ +**thread**. Contains information of threads seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the thread. Prefer using `utid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| utid | INT | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| tid | INT | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|utid\| instead. | +| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| upid | LONG | The process hosting this thread. | +| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | +| machine_id | INT | Machine identifier, non-null for threads on a remote machine. | + +
+ +
+ +
+ +**process**. Contains information of processes seen during the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the process. Prefer using `upid` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| upid | LONG | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | +| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|upid\| instead. | +| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | +| start_ts | LONG | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | +| end_ts | LONG | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | +| parent_upid | INT | The upid of the process which caused this process to be spawned. | +| uid | INT | The Unix user id of the process. | +| android_appid | INT | Android appid of this process. | +| cmdline | STRING | /proc/cmdline for this process. | +| arg_set_id | INT | Extra args for this process. | +| machine_id | INT | Machine identifier, non-null for processes on a remote machine. | + +
+ +
+ +
+ +**args**. Arbitrary key-value pairs which allow adding metadata to other, strongly typed tables. Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| will be non-null. + +
+ +VIEW +Arbitrary key-value pairs which allow adding metadata to other, strongly +typed tables. +Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| +will be non-null. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the arg. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| arg_set_id | INT | The id for a single set of arguments. | +| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | +| key | STRING | The key for the arg. | +| int_value | LONG | The integer value of the arg. | +| string_value | STRING | The string value of the arg. | +| real_value | DOUBLE | The double value of the arg. | +| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | +| display_value | STRING | The human-readable formatted value of the arg. | + +
+ +
+ +
+ +**perf_session**. Contains the Linux perf sessions in the trace. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the perf session. Prefer using `perf_session_id` instead. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| perf_session_id | INT | The id of the perf session. | +| cmdline | STRING | Command line used to collect the data. | + +
+ +
+ +#### Functions + +
+ +**slice_is_ancestor** -\> BOOL. Given two slice ids, returns whether the first is an ancestor of the second. + +
+ +
+ +
+ +Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. + +| Argument | Type | Description | +|---|---|---| +| ancestor_id | LONG | Id of the potential ancestor slice. | +| descendant_id | LONG | Id of the potential descendant slice. | + +
+ +
+ +
+ +**trace_start** -\> LONG. Fetch start of the trace. + +
+ +
+ +
+ +Returns LONG: Start of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_end** -\> LONG. Fetch end of the trace. + +
+ +
+ +
+ +Returns LONG: End of the trace in nanoseconds. + +
+ +
+ +
+ +**trace_dur** -\> LONG. Fetch duration of the trace. + +
+ +
+ +
+ +Returns LONG: Duration of the trace in nanoseconds. + +
+ +
+ +#### Macros + +
+ +**cast_int** . Casts \|value\| to INT. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +
+ +**cast_double** . Casts \|value\| to DOUBLE. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +
+ +**cast_string** . Casts \|value\| to STRING. + +
+ +
+ +
+ +Returns: Expr, + +| Argument | Type | Description | +|---|---|---| +| value | Expr | Query or subquery that will be cast. | + +
+ +
+ +## Package: v8 + +### v8.jit + +#### Views/Tables + +
+ +**v8_isolate**. A V8 Isolate instance + +
+ +VIEW +A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8 +engine. + +| Column | Type | Description | +|---|---|---| +| v8_isolate_id | UINT | Unique V8 isolate id. | +| upid | UINT | Process the isolate was created in. | +| internal_isolate_id | UINT | Internal id used by the v8 engine. Unique in a process. | +| embedded_blob_code_start_address | LONG | Absolute start address of the embedded code blob. | +| embedded_blob_code_size | LONG | Size in bytes of the embedded code blob. | +| code_range_base_address | LONG | Base address of the code range if the isolate defines one. | +| code_range_size | LONG | Size of a code range if the isolate defines one. | +| shared_code_range | LONG | Whether the code range for this Isolate is shared with others in the same process. There is at max one such shared code range per process. | +| embedded_blob_code_copy_start_address | LONG | Used when short builtin calls are enabled, where embedded builtins are copied into the CodeRange so calls can be nearer. | + +
+ +
+ +
+ +**v8_js_script**. Represents a script that was compiled to generate code + +
+ +VIEW +Represents a script that was compiled to generate code. Some V8 code is +generated out of scripts and will reference a V8Script other types of code +will not (e.g. builtins). + +| Column | Type | Description | +|---|---|---| +| v8_js_script_id | UINT | Unique V8 JS script id. | +| v8_isolate_id | UINT | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | +| script_type | STRING | Script type. | +| name | STRING | Script name. | +| source | STRING | Actual contents of the script. | + +
+ +
+ +
+ +**v8_wasm_script**. Represents one WASM script. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| v8_wasm_script_id | UINT | Unique V8 WASM script id. | +| v8_isolate_id | UINT | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | +| internal_script_id | UINT | Script id used by the V8 engine. | +| url | STRING | URL of the source. | +| source | STRING | Actual contents of the script. | + +
+ +
+ +
+ +**v8_js_function**. Represents a v8 Javascript function. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| v8_js_function_id | UINT | Unique V8 JS function id. | +| name | STRING | Function name. | +| v8_js_script_id | UINT | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | +| is_toplevel | BOOL | Whether this function represents the top level script. | +| kind | STRING | Function kind (e.g. regular function or constructor). | +| line | UINT | Line in script where function is defined. Starts at 1. | +| col | UINT | Column in script where function is defined. Starts at 1. | + +
+ +
+ +## Package: wattson + +### wattson.system_state + +#### Views/Tables + +
+ +**wattson_system_states**. The final system state for the CPU subsystem, which has all the information needed by Wattson to estimate energy for the CPU subsystem. + +
+ +TABLE +The final system state for the CPU subsystem, which has all the information +needed by Wattson to estimate energy for the CPU subsystem. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Starting timestamp of the current counter where system state is constant. | +| dur | INT | Duration of the current counter where system state is constant. | +| l3_hit_count | INT | Number of L3 hits the current system state. | +| l3_miss_count | INT | Number of L3 misses in the current system state. | +| freq_0 | INT | Frequency of CPU0. | +| idle_0 | INT | Idle state of CPU0. | +| freq_1 | INT | Frequency of CPU1. | +| idle_1 | INT | Idle state of CPU1. | +| freq_2 | INT | Frequency of CPU2. | +| idle_2 | INT | Idle state of CPU2. | +| freq_3 | INT | Frequency of CPU3. | +| idle_3 | INT | Idle state of CPU3. | +| freq_4 | INT | Frequency of CPU4. | +| idle_4 | INT | Idle state of CPU4. | +| freq_5 | INT | Frequency of CPU5. | +| idle_5 | INT | Idle state of CPU5. | +| freq_6 | INT | Frequency of CPU6. | +| idle_6 | INT | Idle state of CPU6. | +| freq_7 | INT | Frequency of CPU7. | +| idle_7 | INT | Idle state of CPU7. | +| suspended | BOOL | Flag indicating if current system state is suspended. | + +
+ +
+ +## Package: stacks + +### stacks.cpu_profiling + +#### Views/Tables + +
+ +**cpu_profiling_samples**. Table containing all the timestamped samples of CPU profiling which occurred during the trace. Currently, this table is backed by the following data sources: \* Linux perf \* macOS instruments \* Chrome CPU profiling \* Legacy V8 CPU profiling \* Profiling data in Gecko traces + +
+ +TABLE +Table containing all the timestamped samples of CPU profiling which occurred +during the trace. + +Currently, this table is backed by the following data sources: +\* Linux perf +\* macOS instruments +\* Chrome CPU profiling +\* Legacy V8 CPU profiling +\* Profiling data in Gecko traces + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the sample. | +| ts | INT | The timestamp of the sample. | +| utid | INT | The utid of the thread of the sample, if available. | +| tid | INT | The tid of the sample, if available. | +| thread_name | STRING | The thread name of thread of the sample, if available. | +| ucpu | INT | The ucpu of the sample, if available. | +| cpu | INT | The cpu of the sample, if available. | +| callsite_id | INT | The callsite id of the sample. | + +
+ +
+ +
+ +**cpu_profiling_summary_tree** . Table summarising the callstacks captured during any CPU profiling which occurred during the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. The data sources supported are the same as the `cpu_profiling_samples` table. + +
+ +TABLE +Table summarising the callstacks captured during any CPU profiling which +occurred during the trace. + +Specifically, this table returns a tree containing all the callstacks seen +during the trace with `self_count` equal to the number of samples with that +frame as the leaf and `cumulative_count` equal to the number of samples with +the frame anywhere in the tree. + +The data sources supported are the same as the `cpu_profiling_samples` table. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | +| parent_id | INT | The id of the parent callstack for this callstack. NULL if this is root. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
+ +## Package: pkvm + +### pkvm.hypervisor + +#### Views/Tables + +
+ +**pkvm_hypervisor_events**. Events when CPU entered hypervisor. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the corresponding slice in slices table. | +| cpu | INT | CPU that entered hypervisor. | +| ts | INT | Timestamp when CPU entered hypervisor (in nanoseconds). | +| dur | INT | How much time CPU spent in hypervisor (in nanoseconds). | +| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | + +
+ +
+ +## Package: slices + +### slices.cpu_time + +#### Views/Tables + +
+ +**thread_slice_cpu_time**. Time each thread slice spent running on CPU. Requires scheduling data to be available in the trace. + +
+ +TABLE +Time each thread slice spent running on CPU. +Requires scheduling data to be available in the trace. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| cpu_time | INT | Duration of the time the slice was running. | + +
+ +
+ +
+ +**thread_slice_cpu_cycles**. CPU cycles per each slice. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +### slices.with_context + +#### Views/Tables + +
+ +**thread_slice**. All thread slices with data about thread, thread track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All thread slices with data about thread, thread track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `thread_track.name`. | +| thread_name | STRING | Alias for `thread.name`. | +| utid | INT | Alias for `thread.utid`. | +| tid | INT | Alias for `thread.tid`. | +| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +
+ +**process_slice**. All process slices with data about process track and process. Where possible, use available view functions which filter this view. + +
+ +VIEW +All process slices with data about process track and process. +Where possible, use available view functions which filter this view. + +| Column | Type | Description | +|---|---|---| +| id | INT | Alias for `slice.id`. | +| type | STRING | Alias for `slice.type`. | +| ts | INT | Alias for `slice.ts`. | +| dur | INT | Alias for `slice.dur`. | +| category | STRING | Alias for `slice.category`. | +| name | STRING | Alias for `slice.name`. | +| track_id | INT | Alias for `slice.track_id`. | +| track_name | STRING | Alias for `process_track.name`. | +| process_name | STRING | Alias for `process.name`. | +| upid | INT | Alias for `process.upid`. | +| pid | INT | Alias for `process.pid`. | +| depth | INT | Alias for `slice.depth`. | +| parent_id | INT | Alias for `slice.parent_id`. | +| arg_set_id | INT | Alias for `slice.arg_set_id`. | +| thread_ts | INT | Alias for `slice.thread_ts`. | +| thread_dur | INT | Alias for `slice.thread_dur`. | + +
+ +
+ +## Package: sched + +### sched.runnable + +#### Views/Tables + +
+ +**sched_previous_runnable_on_thread**. Previous runnable slice on the same thread. For each "Running" thread state finds: - previous "Runnable" (or runnable preempted) state. - previous uninterrupted "Runnable" state with a valid waker thread. + +
+ +TABLE +Previous runnable slice on the same thread. +For each "Running" thread state finds: +- previous "Runnable" (or runnable preempted) state. +- previous uninterrupted "Runnable" state with a valid waker thread. + +| Column | Type | Description | +|---|---|---| +| id | INT | `thread_state.id` id. | +| prev_runnable_id | INT | Previous runnable `thread_state.id`. | +| prev_wakeup_runnable_id | INT | Previous runnable `thread_state.id` with valid waker thread. | + +
+ +
+ +### sched.states + +#### Functions + +
+ +**sched_state_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state to a human-readable string. + +
+ +
+ +
+ +Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). + +| Argument | Type | Description | +|---|---|---| +| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | + +
+ +
+ +
+ +**sched_state_io_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state and IO wait information to a human-readable string. + +
+ +
+ +
+ +Translates a single-letter scheduling state and IO wait information to +a human-readable string. +Returns STRING: A human readable string with information about the scheduling state and IO wait. + +| Argument | Type | Description | +|---|---|---| +| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | + +
+ +
+ +### sched.thread_level_parallelism + +#### Views/Tables + +
+ +**sched_runnable_thread_count**. The count of runnable threads over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the runnable thread count changed to the current value. | +| runnable_thread_count | INT | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +
+ +**sched_active_cpu_count**. The count of active CPUs over time. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp when the number of active CPU changed. | +| active_cpu_count | INT | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | + +
+ +
+ +### sched.time_in_state + +#### Views/Tables + +
+ +**sched_time_in_state_for_thread**. The time a thread spent in each scheduling state during it's lifetime. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| total_runtime | INT | Total runtime of thread. | +| state | STRING | One of the scheduling states of kernel thread. | +| time_in_state | INT | Total time spent in the scheduling state. | +| percentage_in_state | INT | Percentage of time thread spent in scheduling state in \[0-100\] range. | + +
+ +
+ +
+ +**sched_percentage_of_time_in_state**. Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] ranges) + +
+ +TABLE +Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] +ranges). Sum of all states might be smaller than 100, as those values +are rounded down. + +| Column | Type | Description | +|---|---|---| +| utid | INT | Utid of the thread. | +| running | INT | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | +| runnable | INT | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | +| runnable_preempted | INT | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | +| sleeping | INT | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | +| uninterruptible_sleep | INT | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | +| other | INT | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | + +
+ +
+ +#### Table Functions + +
+ +**sched_time_in_state_for_thread_in_interval** . Time the thread spent each state in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_and_cpu_for_thread_in_interval** . Time the thread spent each state and cpu in a given interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | The start of the interval. | +| dur | INT | The duration of the interval. | +| utid | INT | The utid of the thread. | + +| Column | Type | Description | +|---|---|---| +| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | +| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | +| cpu | INT | Id of the CPU. | +| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | +| dur | INT | Total time spent with this state, cpu and blocked function. | + +
+ +
+ +
+ +**sched_time_in_state_for_cpu_in_interval** . Time spent by CPU in each scheduling state in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| cpu | INT | CPU id. | +| ts | INT | Interval start. | +| dur | INT | Interval duration. | + +| Column | Type | Description | +|---|---|---| +| end_state | STRING | End state. From `sched.end_state`. | +| dur | INT | Duration in state. | + +
+ +
+ +## Package: intervals + +### intervals.overlap + +#### Macros + +
+ +**intervals_overlap_count** . Compute the distribution of the overlap of the given intervals over time. Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) counter, with the value corresponding to the number of intervals that overlap the given timestamp and interval until the next timestamp. + +
+ +
+ +
+ +Compute the distribution of the overlap of the given intervals over time. + +Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) +counter, with the value corresponding to the number of intervals that overlap +the given timestamp and interval until the next timestamp. +Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. + +| Argument | Type | Description | +|---|---|---| +| segments | TableOrSubquery | Table or subquery containing interval data. | +| ts_column | ColumnName | Column containing interval starts (usually `ts`). | +| dur_column | ColumnName | Column containing interval durations (usually `dur`). | + +
+ +
+ +## Package: time + +### time.conversion + +#### Functions + +
+ +**time_from_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_from_us** -\> INT. Converts a duration in microseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in microseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| micros | INT | Time duration in microseconds. | + +
+ +
+ +
+ +**time_from_ms** -\> INT. Converts a duration in millseconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in millseconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| millis | INT | Time duration in milliseconds. | + +
+ +
+ +
+ +**time_from_s** -\> INT. Converts a duration in seconds to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in seconds to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| seconds | INT | Time duration in seconds. | + +
+ +
+ +
+ +**time_from_min** -\> INT. Converts a duration in minutes to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in minutes to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| minutes | INT | Time duration in minutes. | + +
+ +
+ +
+ +**time_from_hours** -\> INT. Converts a duration in hours to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in hours to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| hours | INT | Time duration in hours. | + +
+ +
+ +
+ +**time_from_days** -\> INT. Converts a duration in days to nanoseconds, which is the default representation of time durations in trace processor. + +
+ +
+ +
+ +Converts a duration in days to nanoseconds, which is the default +representation of time durations in trace processor. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| days | INT | Time duration in days. | + +
+ +
+ +
+ +**time_to_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor + +
+ +
+ +
+ +Returns the provided nanosecond duration, which is the default +representation of time durations in trace processor. Provided for +consistency with other functions. +Returns INT: Time duration in nanoseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_us** -\> INT. Converts a duration in nanoseconds to microseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to microseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in microseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_ms** -\> INT. Converts a duration in nanoseconds to millseconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to millseconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in milliseconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_s** -\> INT. Converts a duration in nanoseconds to seconds + +
+ +
+ +
+ +Converts a duration in nanoseconds to seconds. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in seconds. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_min** -\> INT. Converts a duration in nanoseconds to minutes + +
+ +
+ +
+ +Converts a duration in nanoseconds to minutes. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in minutes. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_hours** -\> INT. Converts a duration in nanoseconds to hours + +
+ +
+ +
+ +Converts a duration in nanoseconds to hours. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in hours. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +
+ +**time_to_days** -\> INT. Converts a duration in nanoseconds to days + +
+ +
+ +
+ +Converts a duration in nanoseconds to days. Nanoseconds is the default +representation of time durations in trace processor. +Returns INT: Time duration in days. + +| Argument | Type | Description | +|---|---|---| +| nanos | INT | Time duration in nanoseconds. | + +
+ +
+ +## Package: linux + +### linux.cpu.frequency + +#### Views/Tables + +
+ +**cpu_frequency_counters**. Counter information for each frequency change for each CPU + +
+ +TABLE +Counter information for each frequency change for each CPU. Finds each time +region where a CPU frequency is constant. + +| Column | Type | Description | +|---|---|---| +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter | +| dur | INT | Duration in which counter is constant and frequency doesn't change. | +| freq | INT | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | +| ucpu | INT | Unique CPU id. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
+ +### linux.cpu.idle + +#### Views/Tables + +
+ +**cpu_idle_counters**. Counter information for each idle state change for each CPU + +
+ +TABLE +Counter information for each idle state change for each CPU. Finds each time +region where a CPU idle state is constant. + +| Column | Type | Description | +|---|---|---| +| id | INT | Counter id. | +| track_id | INT | Joinable with 'counter_track.id'. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which the counter is contant and idle state doesn't change. | +| idle | INT | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | +| cpu | INT | CPU that corresponds to this counter. | + +
+ +
+ +### linux.cpu.idle_stats + +#### Views/Tables + +
+ +**cpu_idle_stats**. Aggregates cpu idle statistics per core. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| cpu | INT | CPU core number. | +| state | INT | CPU idle state (C-states). | +| count | INT | The count of entering idle state. | +| dur | INT | Total CPU core idle state duration in nanoseconds. | +| avg_dur | INT | Average CPU core idle state duration in nanoseconds. | +| idle_percent | FLOAT | Idle state percentage of non suspend time (C-states + P-states). | + +
+ +
+ +### linux.cpu.idle_time_in_state + +#### Views/Tables + +
+ +**cpu_idle_time_in_state_counters**. Counter information for sysfs cpuidle states. Tracks the percentage of time spent in each state between two timestamps, by dividing the incremental time spent in one state, by time all CPUS spent in any state. + +
+ +TABLE +Counter information for sysfs cpuidle states. +Tracks the percentage of time spent in each state between two timestamps, by +dividing the incremental time spent in one state, by time all CPUS spent in +any state. + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Timestamp. | +| state_name | STRING | State name. | +| idle_percentage | DOUBLE | Percentage of time all CPUS spent in this state. | +| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | +| time_slice | INT | Time all CPUS spent in any state, in microseconds. | + +
+ +
+ +### linux.cpu.utilization.process + +#### Views/Tables + +
+ +**cpu_cycles_per_process**. Aggregated CPU statistics for each process. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Unique process id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +#### Table Functions + +
+ +**cpu_process_utilization_per_period** . Returns a table of process utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of process utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +upid \| INT \| Upid of the process. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_process_utilization_per_second** . Returns a table of process utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of process utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of the process. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_per_process_in_interval** . Aggregated CPU statistics for each process in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| upid | INT | Unique process id. Joinable with `process.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +### linux.cpu.utilization.slice + +#### Views/Tables + +
+ +**cpu_cycles_per_thread_slice**. CPU cycles per each slice. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +#### Table Functions + +
+ +**cpu_cycles_per_thread_slice_in_interval** . CPU cycles per each slice in interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| id | INT | Id of a slice. Alias of `slice.id`. | +| name | STRING | Name of the slice. | +| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | +| thread_name | STRING | Name of the thread. | +| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | +| process_name | STRING | Name of the process. | +| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | +| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | + +
+ +
+ +### linux.cpu.utilization.system + +#### Views/Tables + +
+ +**cpu_utilization_per_second**. Table with system utilization per second. Utilization is calculated by sum of average utilization of each CPU every second + +
+ +TABLE +Table with system utilization per second. +Utilization is calculated by sum of average utilization of each CPU every +second. For this reason first and last second might have lower then real +utilization. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles**. Aggregated CPU statistics for whole trace + +
+ +TABLE +Aggregated CPU statistics for whole trace. Results in only one row. + +| Column | Type | Description | +|---|---|---| +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +
+ +**cpu_cycles_per_cpu**. Aggregated CPU statistics for each CPU. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | The number of the CPU. Might not be the same as ucpu in multi machine cases. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +#### Table Functions + +
+ +**cpu_utilization_per_period** . Returns a table of system utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of system utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_in_interval** . Aggregated CPU statistics in a provided interval + +
+ +
+ +
+ +Aggregated CPU statistics in a provided interval. Results in one row. +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Start of the interval. +dur \| INT \| Duration of the interval. + +| Column | Type | Description | +|---|---|---| +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on all CPUs. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +
+ +**cpu_cycles_per_cpu_in_interval** . Aggregated CPU statistics for each CPU in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | +| cpu | INT | CPU number. | +| millicycles | INT | Sum of CPU millicycles. | +| megacycles | INT | Sum of CPU megacycles. | +| runtime | INT | Total runtime of all threads running on CPU. | +| min_freq | INT | Minimum CPU frequency in kHz. | +| max_freq | INT | Maximum CPU frequency in kHz. | +| avg_freq | INT | Average CPU frequency in kHz. | + +
+ +
+ +### linux.cpu.utilization.thread + +#### Views/Tables + +
+ +**cpu_cycles_per_thread**. Aggregated CPU statistics for each thread. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| utid | INT | Unique thread id | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +#### Table Functions + +
+ +**cpu_thread_utilization_per_period** . Returns a table of thread utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of thread utilization per given period. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +interval \| INT \| Length of the period on which utilization should be averaged. +utid \| INT \| Utid of the thread. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_thread_utilization_per_second** . Returns a table of thread utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| + +
+ +
+ +
+ +Returns a table of thread utilization per second. +Utilization is calculated as sum of average utilization of each CPU in each +period, which is defined as a multiply of \|interval\|. For this reason +first and last period might have lower then real utilization. +Argument \| Type \| Description +--- \| --- \| --- +utid \| INT \| Utid of the thread. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp of start of a second. | +| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | +| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | + +
+ +
+ +
+ +**cpu_cycles_per_thread_in_interval** . Aggregated CPU statistics for each thread in a provided interval. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| ts | INT | Start of the interval. | +| dur | INT | Duration of the interval. | + +| Column | Type | Description | +|---|---|---| +| utid | INT | Unique thread id. Joinable with `thread.id`. | +| millicycles | INT | Sum of CPU millicycles | +| megacycles | INT | Sum of CPU megacycles | +| runtime | INT | Total runtime duration | +| min_freq | INT | Minimum CPU frequency in kHz | +| max_freq | INT | Maximum CPU frequency in kHz | +| avg_freq | INT | Average CPU frequency in kHz | + +
+ +
+ +### linux.devfreq + +#### Views/Tables + +
+ +**linux_devfreq_dsu_counter**. ARM DSU device frequency counters + +
+ +TABLE +ARM DSU device frequency counters. This table will only be populated on +traces collected with "devfreq/devfreq_frequency" ftrace event enabled, +and from ARM devices with the DSU (DynamIQ Shared Unit) hardware. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| dsu_freq | INT | Frequency in kHz of the device that corresponds to the counter. | + +
+ +
+ +#### Table Functions + +
+ +**linux_get_devfreq_counters** . Gets devfreq frequency counter based on device queried + +
+ +
+ +
+ +Gets devfreq frequency counter based on device queried. These counters will +only be available if the "devfreq/devfreq_frequency" ftrace event is enabled. +Argument \| Type \| Description +--- \| --- \| --- +device_name \| STRING \| Devfreq name to query for. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this counter. | +| ts | LONG | Starting timestamp of the counter. | +| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | +| freq | INT | Frequency in kHz of the device that corresponds to the counter. | + +
+ +
+ +### linux.memory.high_watermark + +#### Views/Tables + +
+ +**memory_rss_high_watermark_per_process**. For each process fetches the memory high watermark until or during timestamp. + +
+ +VIEW +For each process fetches the memory high watermark until or during +timestamp. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | +| process_name | STRING | Name of the process | +| rss_high_watermark | INT | Maximum `rss` value until now | + +
+ +
+ +### linux.memory.process + +#### Views/Tables + +
+ +**memory_rss_and_swap_per_process**. Memory metrics timeline for each process. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| pid | INT | Pid of the process | +| process_name | STRING | Name of the process | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
+ +### linux.perf.samples + +#### Views/Tables + +
+ +**linux_perf_samples_summary_tree** . Table summarising the callstacks captured during all perf samples in the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. + +
+ +TABLE +Table summarising the callstacks captured during all +perf samples in the trace. + +Specifically, this table returns a tree containing all +the callstacks seen during the trace with `self_count` +equal to the number of samples with that frame as the +leaf and `cumulative_count` equal to the number of +samples with the frame anywhere in the tree. + +| Column | Type | Description | +|---|---|---| +| id | INT | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | +| parent_id | INT | The id of the parent callstack for this callstack. | +| name | STRING | The function name of the frame for this callstack. | +| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | +| source_file | STRING | The name of the file containing the function. | +| line_number | INT | The line number in the file the function is located at. | +| self_count | INT | The number of samples with this function as the leaf frame. | +| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | + +
+ +
+ +### linux.perf.spe + +#### Views/Tables + +
+ +**linux_perf_spe_record**. Contains ARM Statistical Profiling Extension records + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | LONG | Timestap when the operation was sampled | +| utid | INT | Thread the operation executed in | +| exception_level | STRING | Exception level the instruction was executed in | +| instruction_frame_id | INT | Instruction virtual address | +| operation | STRING | Type of operation sampled | +| data_virtual_address | LONG | The virtual address accessed by the operation (0 if no memory access was performed) | +| data_physical_address | LONG | The physical address accessed by the operation (0 if no memory access was performed) | +| total_latency | INT | Cycle count from the operation being dispatched for issue to the operation being complete. | +| issue_latency | INT | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | +| translation_latency | INT | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | +| data_source | STRING | Where the data returned for a load operation was sourced | +| exception_gen | BOOL | Operation generated an exception | +| retired | BOOL | Operation architecturally retired | +| l1d_access | BOOL | Operation caused a level 1 data cache access | +| l1d_refill | BOOL | Operation caused a level 1 data cache refill | +| tlb_access | BOOL | Operation caused a TLB access | +| tlb_refill | BOOL | Operation caused a TLB refill involving at least one translation table walk | +| not_taken | BOOL | Conditional instruction failed its condition code check | +| mispred | BOOL | Whether a branch caused a correction to the predicted program flow | +| llc_access | BOOL | Operation caused a last level data or unified cache access | +| llc_refill | BOOL | Whether the operation could not be completed by the last level data cache (or any above) | +| remote_access | BOOL | Operation caused an access to another socket in a multi-socket system | +| alignment | BOOL | Operation that incurred additional latency due to the alignment of the address and the size of the data being accessed | +| tme_transaction | BOOL | Whether the operation executed in transactional state | +| sve_partial_pred | BOOL | SVE or SME operation with at least one false element in the governing predicate(s) | +| sve_empty_pred | BOOL | SVE or SME operation with no true element in the governing predicate(s) | +| l2d_access | BOOL | Whether a load operation caused a cache access to at least the level 2 data or unified cache | +| l2d_hit | BOOL | Whether a load operation accessed and missed the level 2 data or unified cache. Not set for accesses that are satisfied from refilling data of a previous miss | +| cache_data_modified | BOOL | Whether a load operation accessed modified data in a cache | +| recenty_fetched | BOOL | Wheter a load operation hit a recently fetched line in a cache | +| data_snooped | BOOL | Whether a load operation snooped data from a cache outside the cache hierarchy of this core | + +
+ +
+ +### linux.threads + +#### Views/Tables + +
+ +**linux_kernel_threads**. All kernel threads of the trace + +
+ +TABLE +All kernel threads of the trace. As kernel threads are processes, provides +also process data. + +| Column | Type | Description | +|---|---|---| +| upid | INT | Upid of kernel thread. Alias of \|process.upid\|. | +| utid | INT | Utid of kernel thread. Alias of \|thread.utid\|. | +| pid | INT | Pid of kernel thread. Alias of \|process.pid\|. | +| tid | INT | Tid of kernel thread. Alias of \|process.pid\|. | +| process_name | STRING | Name of kernel process. Alias of \|process.name\|. | +| thread_name | STRING | Name of kernel thread. Alias of \|thread.name\|. | +| machine_id | INT | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of \|process.machine_id\|. | + +
+ +
+ +## Package: android + +### android.anrs + +#### Views/Tables + +
+ +**android_anrs**. List of all ANRs that occurred in the trace (one row per ANR). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that triggered the ANR. | +| pid | INT | PID of the process that triggered the ANR. | +| upid | INT | UPID of the process that triggered the ANR. | +| error_id | STRING | UUID of the ANR (generated on the platform). | +| ts | INT | Timestamp of the ANR. | +| subject | STRING | Subject line of the ANR. | + +
+ +
+ +### android.app_process_starts + +#### Views/Tables + +
+ +**android_app_process_starts**. All app cold starts with information about their cold start reason: broadcast, service, activity or provider. + +
+ +TABLE +All app cold starts with information about their cold start reason: +broadcast, service, activity or provider. + +| Column | Type | Description | +|---|---|---| +| start_id | INT | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | +| id | INT | Slice id of intent received in the app. | +| track_id | INT | Track id of the intent received in the app. | +| process_name | STRING | Name of the process receiving the intent. | +| pid | INT | Pid of the process receiving the intent. | +| upid | INT | Upid of the process receiving the intent. | +| intent | STRING | Intent action or component responsible for the cold start. | +| reason | STRING | Process start reason: activity, broadcast, service or provider. | +| proc_start_ts | INT | Timestamp the process start was dispatched from system_server. | +| proc_start_dur | INT | Duration to dispatch the process start from system_server. | +| bind_app_ts | INT | Timestamp the bindApplication started in the app. | +| bind_app_dur | INT | Duration to complete bindApplication in the app. | +| intent_ts | INT | Timestamp the Intent was received in the app. | +| intent_dur | INT | Duration to handle intent in the app. | +| total_dur | INT | Total duration from proc_start dispatched to intent completed. | + +
+ +
+ +### android.auto.multiuser + +#### Views/Tables + +
+ +**android_auto_multiuser_timing**. Time elapsed between the latest user start and the specific end event like package startup(ex carlauncher) or previous user stop. + +
+ +TABLE +Time elapsed between the latest user start +and the specific end event +like package startup(ex carlauncher) or previous user stop. + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Id of the started android user | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | + +
+ +
+ +
+ +**android_auto_multiuser_timing_with_previous_user_resource_usage** . This table extends `android_auto_multiuser_timing` table with previous user resource usage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| event_start_user_id | STRING | Start user id | +| event_start_time | INT | Start event time | +| event_end_time | INT | End event time | +| event_end_name | STRING | End event name | +| event_start_name | STRING | Start event name | +| duration | LONG | User switch duration from start event to end event | +| user_id | INT | User id | +| total_cpu_time | LONG | Total CPU time for a user | +| total_memory_usage_kb | LONG | Total memory user for a user | + +
+ +
+ +### android.battery + +#### Views/Tables + +
+ +**android_battery_charge**. Battery charge at timestamp. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp. | +| current_avg_ua | DOUBLE | Current average micro ampers. | +| capacity_percent | DOUBLE | Current capacity percentage. | +| charge_uah | DOUBLE | Current charge in micro ampers. | +| current_ua | DOUBLE | Current micro ampers. | + +
+ +
+ +### android.battery_stats + +#### Views/Tables + +
+ +**android_battery_stats_state**. View of human readable battery stats counter-based states + +
+ +VIEW +View of human readable battery stats counter-based states. These are recorded +by BatteryStats as a bitmap where each 'category' has a unique value at any +given time. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| value | INT | The counter value as a number. | +| value_name | STRING | The counter value as a human-readable string. | + +
+ +
+ +
+ +**android_battery_stats_event_slices**. View of slices derived from battery_stats events + +
+ +VIEW +View of slices derived from battery_stats events. Battery stats records all +events as instants, however some may indicate whether something started or +stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground +apps or long wakes include these details and allow drawing slices between +instant events found in a trace. + +For example, we may see an event like the following on 'battery_stats.top': + + -top=10215:"com.google.android.apps.nexuslauncher" + +This view will find the associated start ('+top') with the matching suffix +(everything after the '=') to construct a slice. It computes the timestamp +and duration from the events and extract the details as follows: + + track_name='battery_stats.top' + str_value='com.google.android.apps.nexuslauncher' + int_value=10215 + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | The duration the state was active, may be negative for incomplete slices. | +| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | +| track_name | STRING | The name of the counter track. | +| str_value | STRING | String value. | +| int_value | INT | Int value. | + +
+ +
+ +#### Functions + +
+ +**android_battery_stats_counter_to_string** -\> STRING. Converts a battery_stats counter value to human readable string. + +
+ +
+ +
+ +Returns STRING: The human-readable name for the counter value. + +| Argument | Type | Description | +|---|---|---| +| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | +| value | FLOAT | The counter value. | + +
+ +
+ +### android.binder + +#### Views/Tables + +
+ +**android_binder_metrics_by_process**. Count Binder transactions per process. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| process_name | STRING | Name of the process that started the binder transaction. | +| pid | INT | PID of the process that started the binder transaction. | +| slice_name | STRING | Name of the slice with binder transaction. | +| event_count | INT | Number of binder transactions in process in slice. | + +
+ +
+ +
+ +**android_sync_binder_thread_state_by_txn**. Aggregated thread_states on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the thread state durations. The \|thread_state_type\| column represents whether a given 'aggregated thread_state' row is on the client or server side + +
+ +VIEW +Aggregated thread_states on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the thread state durations. +The \|thread_state_type\| column represents whether a given 'aggregated thread_state' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client timestamp | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server timestamp | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| thread_state | STRING | a thread_state that occurred in the txn | +| thread_state_dur | INT | aggregated dur of the \|thread_state\| in the txn | +| thread_state_count | INT | aggregated count of the \|thread_state\| in the txn | + +
+ +
+ +
+ +**android_sync_binder_blocked_functions_by_txn**. Aggregated blocked_functions on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the kernel blocked function durations. The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' row is on the client or server side + +
+ +VIEW +Aggregated blocked_functions on the client and server side per binder txn +This builds on the data from \|_sync_binder_metrics_by_txn\| and +for each end (client and server) of the transaction, it returns +the aggregated sum of all the kernel blocked function durations. +The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' +row is on the client or server side. 'binder_txn' is client side and 'binder_reply' +is server side. + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | slice id of the binder txn | +| client_ts | INT | Client ts | +| client_tid | INT | Client tid | +| binder_reply_id | INT | slice id of the binder reply | +| server_ts | INT | Server ts | +| server_tid | INT | Server tid | +| thread_state_type | STRING | whether thread state is on the txn or reply side | +| blocked_function | STRING | blocked kernel function in a thread state | +| blocked_function_dur | INT | aggregated dur of the \|blocked_function\| in the txn | +| blocked_function_count | INT | aggregated count of the \|blocked_function\| in the txn | + +
+ +
+ +
+ +**android_binder_txns**. Breakdown binder transactions per txn. It returns data about the client and server ends of every binder transaction async. + +
+ +TABLE +Breakdown binder transactions per txn. +It returns data about the client and server ends of every binder transaction async. + +| Column | Type | Description | +|---|---|---| +| aidl_name | STRING | name of the binder interface if existing. | +| aidl_ts | INT | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | +| aidl_dur | INT | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | +| binder_txn_id | INT | slice id of the binder txn. | +| client_process | STRING | name of the client process. | +| client_thread | STRING | name of the client thread. | +| client_upid | INT | Upid of the client process. | +| client_utid | INT | Utid of the client thread. | +| client_tid | INT | Tid of the client thread. | +| client_pid | INT | Pid of the client thread. | +| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | +| client_ts | INT | timestamp of the client txn. | +| client_dur | INT | wall clock dur of the client txn. | +| binder_reply_id | INT | slice id of the binder reply. | +| server_process | STRING | name of the server process. | +| server_thread | STRING | name of the server thread. | +| server_upid | INT | Upid of the server process. | +| server_utid | INT | Utid of the server thread. | +| server_tid | INT | Tid of the server thread. | +| server_pid | INT | Pid of the server thread. | +| server_ts | INT | timestamp of the server txn. | +| server_dur | INT | wall clock dur of the server txn. | +| client_oom_score | INT | oom score of the client process at the start of the txn. | +| server_oom_score | INT | oom score of the server process at the start of the reply. | +| is_sync | BOOL | whether the txn is synchronous or async (oneway). | +| client_monotonic_dur | INT | monotonic clock dur of the client txn. | +| server_monotonic_dur | INT | monotonic clock dur of the server txn. | +| client_package_version_code | INT | Client package version_code. | +| server_package_version_code | INT | Server package version_code. | +| is_client_package_debuggable | INT | Whether client package is debuggable. | +| is_server_package_debuggable | INT | Whether server package is debuggable. | + +
+ +
+ +#### Table Functions + +
+ +**android_binder_outgoing_graph** . Returns a DAG of all outgoing binder txns from a process. The roots of the graph are the threads making the txns and the graph flows from: thread -\> server_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a DAG of all outgoing binder txns from a process. +The roots of the graph are the threads making the txns and the graph flows from: +thread -\> server_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an outgoing graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of outgoing binder txns. | + +
+ +
+ +
+ +**android_binder_incoming_graph** . Returns a DAG of all incoming binder txns from a process. The roots of the graph are the clients making the txns and the graph flows from: client_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a DAG of all incoming binder txns from a process. +The roots of the graph are the clients making the txns and the graph flows from: +client_process -\> AIDL interface -\> AIDL method. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +upid \| INT \| Upid of process to generate an incoming graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of incoming binder txns. | + +
+ +
+ +
+ +**android_binder_graph** . Returns a graph of all binder txns in a trace. The nodes are client_process and server_process. The weights of each node represent the wall execution time in the server_process. + +
+ +
+ +
+ +Returns a graph of all binder txns in a trace. +The nodes are client_process and server_process. +The weights of each node represent the wall execution time in the server_process. +Argument \| Type \| Description +--- \| --- \| --- +min_client_oom_score \| INT \| Matches txns from client_processes greater than or equal to the OOM score. +max_client_oom_score \| INT \| Matches txns from client_processes less than or equal to the OOM score. +min_server_oom_score \| INT \| Matches txns to server_processes greater than or equal to the OOM score. +max_server_oom_score \| INT \| Matches txns to server_processes less than or equal to the OOM score. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of binder txns. | + +
+ +
+ +### android.binder_breakdown + +#### Views/Tables + +
+ +**android_binder_server_breakdown**. Server side binder breakdowns per transactions per txn. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder reply with a single reason. | +| dur | INT | Duration of an exclusive interval during the binder reply with a single reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | + +
+ +
+ +
+ +**android_binder_client_breakdown**. Client side binder breakdowns per transactions per txn. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | +| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | +| ts | INT | Timestamp of an exclusive interval during the binder txn with a single latency reason. | +| dur | INT | Duration of an exclusive interval during the binder txn with a single latency reason. | +| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | + +
+ +
+ +### android.cpu.cluster_type + +#### Views/Tables + +
+ +**android_cpu_cluster_mapping**. Stores the mapping of a cpu to its cluster type - e.g + +
+ +TABLE +Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. +This cluster type is determined by initially using cpu_capacity from sysfs +and grouping clusters with identical capacities, ordered by size. +In the case that capacities are not present, max frequency is used instead. +If nothing is avaiable, NULL is returned. + +| Column | Type | Description | +|---|---|---| +| ucpu | INT | Alias of `cpu.ucpu`. | +| cpu | INT | Alias of `cpu.cpu`. | +| cluster_type | STRING | The cluster type of the CPU. | + +
+ +
+ +### android.device + +#### Views/Tables + +
+ +**android_device_name**. Extract name of the device based on metadata from the trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Device name. | + +
+ +
+ +### android.dvfs + +#### Views/Tables + +
+ +**android_dvfs_counters**. Dvfs counter with duration. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| ts | INT | Timestamp when counter value changed. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | + +
+ +
+ +
+ +**android_dvfs_counter_stats**. Aggregates dvfs counter slice for statistic. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name on which all the other values are aggregated on. | +| max | DOUBLE | Max of all counter values for the counter name. | +| min | DOUBLE | Min of all counter values for the counter name. | +| dur | INT | Duration between the first and last counter value for the counter name. | +| wgt_avg | FLOAT | Weighted avergate of all the counter values for the counter name. | + +
+ +
+ +
+ +**android_dvfs_counter_residency**. Aggregates dvfs counter slice for residency + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | Counter name. | +| value | DOUBLE | Counter value. | +| dur | INT | Counter duration. | +| pct | DOUBLE | Counter duration as a percentage of total duration. | + +
+ +
+ +### android.frames.jank_type + +#### Functions + +
+ +**android_is_sf_jank_type** -\> BOOL. Categorizes whether the jank was caused by Surface Flinger + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents sf jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +
+ +**android_is_app_jank_type** -\> BOOL. Categorizes whether the jank was caused by the app + +
+ +
+ +
+ +Returns BOOL: True when the jank type represents app jank + +| Argument | Type | Description | +|---|---|---| +| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | + +
+ +
+ +### android.frames.per_frame_metrics + +#### Views/Tables + +
+ +**android_frames_overrun**. The amount by which each frame missed of hit its deadline + +
+ +TABLE +The amount by which each frame missed of hit its deadline. Negative if the +deadline was not missed. Frames are considered janky if `overrun` is +positive. +Calculated as the difference between the end of the +`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the +frame. +Availability: from S (API 31). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | + +
+ +
+ +
+ +**android_frames_ui_time**. How much time did the frame's Choreographer callbacks take. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| ui_time | INT | UI time duration | + +
+ +
+ +
+ +**android_app_vsync_delay_per_frame**. App Vsync delay for a frame + +
+ +TABLE +App Vsync delay for a frame. The time between the VSYNC-app signal and the +start of Choreographer work. +Calculated as time difference between the actual frame start (from +`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` +slice. +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | App VSYNC delay. | + +
+ +
+ +
+ +**android_cpu_time_per_frame** . How much time did the frame take across the UI Thread + RenderThread. Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice duration and summed durations of all `DrawFrame` slices associated with this frame. Availability: from N (API 24). For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +How much time did the frame take across the UI Thread + RenderThread. +Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice +duration and summed durations of all `DrawFrame` slices associated with this +frame. +Availability: from N (API 24). +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id | +| app_vsync_delay | INT | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | +| do_frame_dur | INT | Duration of `Choreographer#doFrame` slice. | +| draw_frame_dur | INT | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | +| cpu_time | INT | CPU time across the UI Thread + RenderThread. | + +
+ +
+ +
+ +**android_frame_stats**. Aggregated stats of the frame. For Googlers: more details in go/android-performance-metrics-glossary. + +
+ +TABLE +Aggregated stats of the frame. + +For Googlers: more details in go/android-performance-metrics-glossary. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| overrun | INT | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | +| cpu_time | INT | How much time did the frame take across the UI Thread + RenderThread. | +| ui_time | INT | How much time did the frame's Choreographer callbacks take. | +| was_jank | BOOL | Was frame janky. | +| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | +| was_big_jank | BOOL | CPU time of the frame took over 50ms. | +| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | + +
+ +
+ +### android.frames.timeline + +#### Views/Tables + +
+ +**android_frames_choreographer_do_frame** . All of the `Choreographer#doFrame` slices with their frame id. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| ui_thread_utid | INT | Utid of the UI thread | +| upid | INT | Upid of application process | +| ts | INT | Timestamp of the slice. | + +
+ +
+ +
+ +**android_frames_draw_frame** . All of the `DrawFrame` slices with their frame id and render thread. There might be multiple DrawFrames slices for a single vsync (frame id). This happens when we are drawing multiple layers (e.g + +
+ +TABLE +All of the `DrawFrame` slices with their frame id and render thread. +There might be multiple DrawFrames slices for a single vsync (frame id). +This happens when we are drawing multiple layers (e.g. status bar and +notifications). + +| Column | Type | Description | +|---|---|---| +| id | INT | `slice.id` | +| frame_id | INT | Frame id | +| render_thread_utid | INT | Utid of the render thread | +| upid | INT | Upid of application process | + +
+ +
+ +
+ +**android_frames**. All slices related to one frame + +
+ +TABLE +All slices related to one frame. Aggregates `Choreographer#doFrame`, +`DrawFrame`, `actual_frame_timeline_slice` and +`expected_frame_timeline_slice` slices. +See https://perfetto.dev/docs/data-sources/frametimeline for details. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | +| dur | INT | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | +| actual_frame_timeline_count | INT | Count of slices in `actual_frame_timeline_slice` related to this frame. | +| expected_frame_timeline_count | INT | Count of slices in `expected_frame_timeline_slice` related to this frame. | + +
+ +
+ +#### Table Functions + +
+ +**android_first_frame_after** . Returns first frame after the provided timestamp + +
+ +
+ +
+ +Returns first frame after the provided timestamp. The returning table has at +most one row. +Argument \| Type \| Description +--- \| --- \| --- +ts \| INT \| Timestamp. + +| Column | Type | Description | +|---|---|---| +| frame_id | INT | Frame id. | +| ts | INT | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | +| dur | INT | Duration of the frame. | +| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | +| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | +| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | +| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | +| render_thread_utid | INT | `utid` of the render thread. | +| ui_thread_utid | INT | `utid` of the UI thread. | + +
+ +
+ +### android.freezer + +#### Views/Tables + +
+ +**android_freezer_events**. All frozen processes and their frozen duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Upid of frozen process | +| pid | INT | Pid of frozen process | +| ts | INT | Timestamp process was frozen. | +| dur | INT | Duration process was frozen for. | +| unfreeze_reason_int | INT | Unfreeze reason Integer. | +| unfreeze_reason_str | STRING | Unfreeze reason String. | + +
+ +
+ +### android.garbage_collection + +#### Views/Tables + +
+ +**android_garbage_collection_events**. All Garbage collection events with a breakdown of the time spent and heap reclaimed. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| tid | INT | Tid of thread running garbage collection. | +| pid | INT | Pid of process running garbage collection. | +| utid | INT | Utid of thread running garbage collection. | +| upid | INT | Upid of process running garbage collection. | +| thread_name | STRING | Name of thread running garbage collection. | +| process_name | STRING | Name of process running garbage collection. | +| gc_type | STRING | Type of garbage collection. | +| is_mark_compact | INT | Whether gargage collection is mark compact or copying. | +| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | +| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | +| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | +| gc_id | INT | Garbage collection id. | +| gc_ts | INT | Garbage collection timestamp. | +| gc_dur | INT | Garbage collection wall duration. | +| gc_running_dur | INT | Garbage collection duration spent executing on CPU. | +| gc_runnable_dur | INT | Garbage collection duration spent waiting for CPU. | +| gc_unint_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel on IO. | +| gc_unint_non_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel without IO. | +| gc_int_dur | INT | Garbage collection duration spent waiting in interruptible sleep. | + +
+ +
+ +### android.gpu.frequency + +#### Views/Tables + +
+ +**android_gpu_frequency**. GPU frequency counter per GPU. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| gpu_id | INT | GPU id. Joinable with `gpu_counter_track.gpu_id`. | +| gpu_freq | INT | GPU frequency | + +
+ +
+ +### android.gpu.memory + +#### Views/Tables + +
+ +**android_gpu_memory_per_process**. Counter for GPU memory per process with duration. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| upid | INT | Upid of the process | +| gpu_memory | INT | GPU memory | + +
+ +
+ +### android.input + +#### Views/Tables + +
+ +**android_input_events**. All input events with round trip latency breakdown + +
+ +TABLE +All input events with round trip latency breakdown. Input delivery is socket based and every +input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure +latencies between: +1. Input dispatch event sent from OS. +2. Input dispatch event received in app. +3. Input ACK event sent from app. +4. Input ACk event received in OS. + +| Column | Type | Description | +|---|---|---| +| dispatch_latency_dur | INT | Duration from input dispatch to input received. | +| handling_latency_dur | INT | Duration from input received to input ACK sent. | +| ack_latency_dur | INT | Duration from input ACK sent to input ACK recieved. | +| total_latency_dur | INT | Duration from input dispatch to input event ACK received. | +| end_to_end_latency_dur | INT | Duration from input read to frame present time. Null if an input event has no associated frame event. | +| tid | INT | Tid of thread receiving the input event. | +| thread_name | STRING | Name of thread receiving the input event. | +| pid | INT | Pid of process receiving the input event. | +| process_name | STRING | Name of process receiving the input event. | +| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | +| event_action | STRING | Input event action. | +| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | +| event_channel | STRING | Input event channel name. | +| input_event_id | STRING | Unique identifier for the input event. | +| read_time | INT | Timestamp input event was read by InputReader. | +| dispatch_track_id | INT | Thread track id of input event dispatching thread. | +| dispatch_ts | INT | Timestamp input event was dispatched. | +| dispatch_dur | INT | Duration of input event dispatch. | +| receive_track_id | INT | Thread track id of input event receiving thread. | +| receive_ts | INT | Timestamp input event was received. | +| receive_dur | INT | Duration of input event receipt. | +| frame_id | INT | Vsync Id associated with the input. Null if an input event has no associated frame event. | + +
+ +
+ +
+ +**android_key_events**. Key events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_motion_events**. Motion events processed by the Android framework (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | +| ts | INT | The timestamp of when the input event was processed by the system | +| arg_set_id | INT | Details of the input event parsed from the proto message | + +
+ +
+ +
+ +**android_input_event_dispatch**. Input event dispatching information in Android (from android.input.inputevent data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the trace entry | +| event_id | INT | Event ID of the input event that was dispatched | +| arg_set_id | INT | Extra args parsed from the proto message | +| vsync_id | INT | Vsync ID that identifies the state of the windows during which the dispatch decision was made | +| window_id | INT | Window ID of the window receiving the event | + +
+ +
+ +### android.job_scheduler + +#### Views/Tables + +
+ +**android_job_scheduler_events**. All scheduled jobs and their latencies. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| job_id | INT | Id of the scheduled job assigned by the app developer. | +| uid | INT | Uid of the process running the scheduled job. | +| package_name | STRING | Package name of the process running the scheduled job. | +| job_service_name | STRING | Service component name of the scheduled job. | +| track_id | INT | Thread track id of the job scheduler event slice. | +| id | INT | Slice id of the job scheduler event slice. | +| ts | INT | Timestamp the job was scheduled. | +| dur | INT | Duration of the scheduled job. | + +
+ +
+ +### android.memory.dmabuf + +#### Views/Tables + +
+ +**android_dmabuf_allocs**. Track dmabuf allocations, re-attributing gralloc allocations to their source (if binder transactions to gralloc are recorded). + +
+ +TABLE +Track dmabuf allocations, re-attributing gralloc allocations to their source +(if binder transactions to gralloc are recorded). + +| Column | Type | Description | +|---|---|---| +| ts | INT | timestamp of the allocation | +| buf_size | INT | allocation size (will be negative for release) | +| inode | INT | dmabuf inode | +| utid | INT | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | +| tid | INT | tid of thread responsible for the allocation | +| thread_name | STRING | thread name | +| upid | INT | upid of process responsible for the allocation (matches utid) | +| pid | INT | pid of process responsible for the allocation | +| process_name | STRING | process name | + +
+ +
+ +### android.memory.heap_graph.dominator_tree + +#### Views/Tables + +
+ +**heap_graph_dominator_tree**. All reachable heap graph objects, their immediate dominators and summary of their dominated sets. The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. Each reachable object is a node in the dominator tree, their immediate dominator is their parent node in the tree, and their dominated set is all their descendants in the tree + +
+ +TABLE +All reachable heap graph objects, their immediate dominators and summary of +their dominated sets. +The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. +Each reachable object is a node in the dominator tree, their immediate +dominator is their parent node in the tree, and their dominated set is all +their descendants in the tree. All size information come from the +heap_graph_object prelude table. + +| Column | Type | Description | +|---|---|---| +| id | INT | Heap graph object id. | +| idom_id | INT | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | +| dominated_obj_count | INT | Count of all objects dominated by this object, self inclusive. | +| dominated_size_bytes | INT | Total self_size of all objects dominated by this object, self inclusive. | +| dominated_native_size_bytes | INT | Total native_size of all objects dominated by this object, self inclusive. | +| depth | INT | Depth of the object in the dominator tree. Depth of root objects are 1. | + +
+ +
+ +### android.memory.heap_graph.heap_graph_class_aggregation + +#### Views/Tables + +
+ +**android_heap_graph_class_aggregation**. Class-level breakdown of the java heap. Per type name aggregates the object stats and the dominator tree stats. + +
+ +TABLE +Class-level breakdown of the java heap. +Per type name aggregates the object stats and the dominator tree stats. + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid | +| graph_sample_ts | INT | Heap dump timestamp | +| type_id | INT | Class type id | +| type_name | STRING | Class name (deobfuscated if available) | +| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | +| obj_count | INT | Count of class instances | +| size_bytes | INT | Size of class instances | +| native_size_bytes | INT | Native size of class instances | +| reachable_obj_count | INT | Count of reachable class instances | +| reachable_size_bytes | INT | Size of reachable class instances | +| reachable_native_size_bytes | INT | Native size of reachable class instances | +| dominated_obj_count | INT | Count of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_size_bytes | INT | Size of all objects dominated by instances of this class Only applies to reachable objects | +| dominated_native_size_bytes | INT | Native size of all objects dominated by instances of this class Only applies to reachable objects | + +
+ +
+ +### android.memory.process + +#### Views/Tables + +
+ +**memory_oom_score_with_rss_and_swap_per_process**. Process memory and it's OOM adjuster scores + +
+ +TABLE +Process memory and it's OOM adjuster scores. Detects transitions, each new +interval means that either the memory or OOM adjuster score of the process changed. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score or memory of the process changed | +| dur | INT | Duration until the next oom_adj score or memory change of the process. | +| score | INT | oom adjuster score of the process. | +| bucket | STRING | oom adjuster bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| pid | INT | Pid of the process having an oom_adj update. | +| oom_adj_id | INT | Slice of the latest oom_adj update in the system_server. Alias of `slice.id`. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | +| anon_rss | INT | Anon RSS counter value | +| file_rss | INT | File RSS counter value | +| shmem_rss | INT | Shared memory RSS counter value | +| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | +| swap | INT | Swap counter value | +| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | +| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | + +
+ +
+ +### android.monitor_contention + +#### Views/Tables + +
+ +**android_monitor_contention**. Contains parsed monitor contention slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain**. Contains parsed monitor contention slices with the parent-child relationships. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| parent_id | INT | Id of monitor contention slice blocking this contention. | +| blocking_method | STRING | Name of the method holding the lock. | +| blocked_method | STRING | Blocked_method without arguments and return types. | +| short_blocking_method | STRING | Blocking_method without arguments and return types. | +| short_blocked_method | STRING | Blocked_method without arguments and return types. | +| blocking_src | STRING | File location of blocking_method in form . | +| blocked_src | STRING | File location of blocked_method in form . | +| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | +| blocked_utid | INT | Utid of thread holding the lock. | +| blocked_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_utid | INT | Utid of thread holding the lock. | +| blocking_thread_name | STRING | Thread name of thread holding the lock. | +| blocking_tid | INT | Tid of thread holding the lock. | +| upid | INT | Upid of process experiencing lock contention. | +| process_name | STRING | Process name of process experiencing lock contention. | +| id | INT | Slice id of lock contention. | +| ts | INT | Timestamp of lock contention start. | +| dur | INT | Wall clock duration of lock contention. | +| monotonic_dur | INT | Monotonic clock duration of lock contention. | +| track_id | INT | Thread track id of blocked thread. | +| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | +| blocked_thread_tid | INT | Tid of the blocked thread | +| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | +| blocking_thread_tid | INT | Tid of thread holding the lock. | +| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | +| pid | INT | Pid of process experiencing lock contention. | +| child_id | INT | Id of monitor contention slice blocked by this contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state**. Note that we only span join the duration where the lock was actually held and contended. This can be less than the duration the lock was 'waited on' when a different waiter acquired the lock earlier than the first waiter. + +
+ +TABLE +Note that we only span join the duration where the lock was actually held and contended. +This can be less than the duration the lock was 'waited on' when a different waiter acquired the +lock earlier than the first waiter. + +| Column | Type | Description | +|---|---|---| +| parent_id | None | Id of slice blocking the blocking_thread. | +| blocking_method | None | Name of the method holding the lock. | +| blocked_methhod | None | Name of the method trying to acquire the lock. | +| short_blocking_method | None | Blocking_method without arguments and return types. | +| short_blocked_method | None | Blocked_method without arguments and return types. | +| blocking_src | None | File location of blocking_method in form . | +| blocked_src | None | File location of blocked_method in form . | +| waiter_count | None | Zero indexed number of threads trying to acquire the lock. | +| blocking_utid | None | Utid of the blocking \|thread_state\|. | +| blocking_thread_name | None | Thread name of thread holding the lock. | +| upid | None | Upid of process experiencing lock contention. | +| process_name | None | Process name of process experiencing lock contention. | +| id | None | Slice id of lock contention. | +| ts | None | Timestamp of the blocking \|thread_state\|. | +| dur | None | Wall clock duration of lock contention. | +| monotonic_dur | None | Monotonic clock duration of lock contention. | +| track_id | None | Thread track id of blocked thread. | +| is_blocked_main_thread | None | Whether the blocked thread is the main thread. | +| is_blocking_main_thread | None | Whether the blocking thread is the main thread. | +| binder_reply_id | None | Slice id of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_ts | None | Timestamp of binder reply slice if lock contention was part of a binder txn. | +| binder_reply_tid | None | Tid of binder reply slice if lock contention was part of a binder txn. | +| state | None | Thread state of the blocking thread. | +| blocked_function | None | Blocked kernel function of the blocking thread. | + +
+ +
+ +
+ +**android_monitor_contention_chain_thread_state_by_txn**. Aggregated thread_states on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention slice, it returns the aggregated sum of all the thread states on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated thread_states on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention slice, it returns the aggregated sum of all the thread states on the +blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| thread_state | STRING | A \|thread_state\| that occurred in the blocking thread during the contention. | +| thread_state_dur | INT | Total time the blocking thread spent in the \|thread_state\| during contention. | +| thread_state_count | INT | Count of all times the blocking thread entered \|thread_state\| during the contention. | + +
+ +
+ +
+ +**android_monitor_contention_chain_blocked_functions_by_txn**. Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention, it returns the aggregated sum of all the kernel blocked function durations on the blocking thread. Note that this data is only available for the first waiter on a lock. + +
+ +VIEW +Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. +This builds on the data from \|android_monitor_contention_chain\| and +for each contention, it returns the aggregated sum of all the kernel +blocked function durations on the blocking thread. + +Note that this data is only available for the first waiter on a lock. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id of the monitor contention. | +| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | +| blocked_function_dur | INT | Total time the blocking thread spent in the \|blocked_function\| during the contention. | +| blocked_function_count | INT | Count of all times the blocking thread executed the \|blocked_function\| during the contention. | + +
+ +
+ +#### Functions + +
+ +**android_extract_android_monitor_contention_blocking_thread** -\> STRING. Extracts the blocking thread from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_tid** -\> INT. Extracts the blocking thread tid from a slice name + +
+ +
+ +
+ +Returns INT: Blocking thread tid + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_method** -\> STRING. Extracts the blocking method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocking_method** -\> STRING. Extracts a shortened form of the blocking method name from a slice name. The shortened form discards the parameter and return types. + +
+ +
+ +
+ +Extracts a shortened form of the blocking method name from a slice name. +The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_method** -\> STRING. Extracts the monitor contention blocked method from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_short_blocked_method** -\> STRING. Extracts a shortened form of the monitor contention blocked method name from a slice name + +
+ +
+ +
+ +Extracts a shortened form of the monitor contention blocked method name +from a slice name. The shortened form discards the parameter and return +types. +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_waiter_count** -\> INT. Extracts the number of waiters on the monitor from a slice name + +
+ +
+ +
+ +Returns INT: Count of waiters on the lock + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocking_src** -\> STRING. Extracts the monitor contention blocking source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +
+ +**android_extract_android_monitor_contention_blocked_src** -\> STRING. Extracts the monitor contention blocked source location from a slice name + +
+ +
+ +
+ +Returns STRING: Blocking thread + +| Argument | Type | Description | +|---|---|---| +| slice_name | STRING | Name of slice | + +
+ +
+ +#### Table Functions + +
+ +**android_monitor_contention_graph** . Returns a DAG of all Java lock contentions in a process. Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. + +
+ +
+ +
+ +Returns a DAG of all Java lock contentions in a process. +Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. Argument \| Type \| Description --- \| --- \| --- upid \| INT \| Upid of process to generate a lock graph for. + +| Column | Type | Description | +|---|---|---| +| pprof | BYTES | Pprof of lock graph. | + +
+ +
+ +### android.network_packets + +#### Views/Tables + +
+ +**android_network_packets**. Android network packet events (from android.network_packets data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp in nanoseconds. | +| dur | INT | Duration (non-zero only in aggregate events) | +| track_name | STRING | The track name (interface and direction) | +| package_name | STRING | Traffic package source (or uid=$X if not found) | +| iface | STRING | Traffic interface name (linux interface name) | +| direction | STRING | Traffic direction ('Transmitted' or 'Received') | +| packet_count | INT | Number of packets in this event | +| packet_length | INT | Number of bytes in this event (wire size) | +| packet_transport | STRING | Transport used for traffic in this event | +| packet_tcp_flags | INT | TCP flags used by tcp frames in this event | +| socket_tag | STRING | The Android traffic tag of the network socket | +| socket_uid | INT | The Linux user id of the network socket | +| local_port | INT | The local port number (for udp or tcp only) | +| remote_port | INT | The remote port number (for udp or tcp only) | +| packet_icmp_type | INT | 1-byte ICMP type identifier. | +| packet_icmp_code | INT | 1-byte ICMP code identifier. | +| packet_tcp_flags_int | INT | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | +| socket_tag_int | INT | Packet's socket tag as an integer. | + +
+ +
+ +### android.oom_adjuster + +#### Views/Tables + +
+ +**android_oom_adj_intervals**. All oom adj state intervals across all processes along with the reason for the state update. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp the oom_adj score of the process changed | +| dur | INT | Duration until the next oom_adj score change of the process. | +| score | INT | oom_adj score of the process. | +| bucket | STRING | oom_adj bucket of the process. | +| upid | INT | Upid of the process having an oom_adj update. | +| process_name | STRING | Name of the process having an oom_adj update. | +| oom_adj_id | INT | Slice id of the latest oom_adj update in the system_server. | +| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | +| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | +| oom_adj_track_id | INT | Track id of the latest oom_adj update in the system_server | +| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | +| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | +| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | + +
+ +
+ +#### Functions + +
+ +**android_oom_adj_score_to_bucket_name** -\> STRING. Converts an oom_adj score Integer to String sample name. One of: cached, background, job, foreground_service, bfgs, foreground and system. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String sample name. +One of: cached, background, job, foreground_service, bfgs, foreground and +system. +Returns STRING: Returns the sample bucket based on the oom score. + +| Argument | Type | Description | +|---|---|---| +| oom_score | INT | `oom_score` value | + +
+ +
+ +
+ +**android_oom_adj_score_to_detailed_bucket_name** -\> STRING. Converts an oom_adj score Integer to String bucket name. Deprecated: use `android_oom_adj_score_to_bucket_name` instead. + +
+ +
+ +
+ +Converts an oom_adj score Integer to String bucket name. +Deprecated: use `android_oom_adj_score_to_bucket_name` instead. +Returns STRING: Returns the oom_adj bucket. + +| Argument | Type | Description | +|---|---|---| +| value | INT | oom_adj score. | +| android_appid | INT | android_app id of the process. | + +
+ +
+ +### android.power_rails + +#### Views/Tables + +
+ +**android_power_rails_counters**. Android power rails counters data. For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm NOTE: Requires dedicated hardware - table is only populated on Pixels. + +
+ +TABLE +Android power rails counters data. +For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm +NOTE: Requires dedicated hardware - table is only populated on Pixels. + +| Column | Type | Description | +|---|---|---| +| id | INT | `counter.id` | +| ts | INT | Timestamp of the energy measurement. | +| dur | INT | Time until the next energy measurement. | +| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | +| raw_power_rail_name | STRING | Raw power rail name. | +| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | +| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | +| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | +| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | +| track_id | INT | Power rail track id. Alias of `counter_track.id`. | +| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | + +
+ +
+ +### android.process_metadata + +#### Views/Tables + +
+ +**android_process_metadata**. Data about packages running on the process. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| upid | INT | Process upid. | +| pid | INT | Process pid. | +| process_name | STRING | Process name. | +| uid | INT | Android app UID. | +| shared_uid | BOOL | Whether the UID is shared by multiple packages. | +| package_name | STRING | Name of the packages running in this process. | +| version_code | INT | Package version code. | +| debuggable | INT | Whether package is debuggable. | + +
+ +
+ +### android.screenshots + +#### Views/Tables + +
+ +**android_screenshots**. Screenshot slices, used in perfetto UI. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| ts | INT | Slice timestamp. | +| dur | INT | Slice duration, should be typically 0 since screeenshot slices are of instant type. | +| name | STRING | Slice name. | + +
+ +
+ +### android.services + +#### Views/Tables + +
+ +**android_service_bindings**. All service bindings from client app to server app. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| client_oom_score | INT | OOM score of client process making the binding. | +| client_process | STRING | Name of client process making the binding. | +| client_thread | STRING | Name of client thread making the binding. | +| client_pid | INT | Pid of client process making the binding. | +| client_tid | INT | Tid of client process making the binding. | +| client_upid | INT | Upid of client process making the binding. | +| client_utid | INT | Utid of client thread making the binding. | +| client_ts | INT | Timestamp the client process made the request. | +| client_dur | INT | Duration of the client binding request. | +| server_oom_score | INT | OOM score of server process getting bound to. | +| server_process | STRING | Name of server process getting bound to | +| server_thread | STRING | Name of server thread getting bound to. | +| server_pid | INT | Pid of server process getting bound to. | +| server_tid | INT | Tid of server process getting bound to. | +| server_upid | INT | Upid of server process getting bound to. | +| server_utid | INT | Utid of server process getting bound to. | +| server_ts | INT | Timestamp the server process got bound to. | +| server_dur | INT | Duration of the server process handling the binding. | +| token | STRING | Unique binder identifier for the Service binding. | +| act | STRING | Intent action name for the service binding. | +| cmp | STRING | Intent component name for the service binding. | +| flg | STRING | Intent flag for the service binding. | +| bind_seq | INT | Monotonically increasing id for the service binding. | + +
+ +
+ +### android.slices + +#### Functions + +
+ +**android_standardize_slice_name** -\> STRING. Some slice names have params in them + +
+ +
+ +
+ +Some slice names have params in them. This functions removes them to make it +possible to aggregate by name. +Some examples are: +- Lock/monitor contention slices. The name includes where the lock +contention is in the code. That part is removed. +- DrawFrames/ooFrame. The name also includes the frame number. +- Apk/oat/dex loading: The name of the apk is removed +Returns STRING: Simplified name. + +| Argument | Type | Description | +|---|---|---| +| name | STRING | The raw slice name. | + +
+ +
+ +### android.startup.startups + +#### Views/Tables + +
+ +**android_startups**. All activity startups in the trace by startup id. Populated by different scripts depending on the platform version/contents. + +
+ +TABLE +All activity startups in the trace by startup id. +Populated by different scripts depending on the platform version/contents. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of startup start. | +| ts_end | INT | Timestamp of startup end. | +| dur | INT | Startup duration. | +| package | STRING | Package name. | +| startup_type | STRING | Startup type. | + +
+ +
+ +
+ +**android_startup_processes**. Maps a startup to the set of processes that handled the activity start. The vast majority of cases should be a single process + +
+ +TABLE +Maps a startup to the set of processes that handled the activity start. + +The vast majority of cases should be a single process. However it is +possible that the process dies during the activity startup and is respawned. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| upid | INT | Upid of process on which activity started. | +| startup_type | STRING | Type of the startup. | + +
+ +
+ +
+ +**android_startup_threads**. Maps a startup to the set of threads on processes that handled the activity start. + +
+ +VIEW +Maps a startup to the set of threads on processes that handled the +activity start. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| ts | INT | Timestamp of start. | +| dur | INT | Duration of startup. | +| upid | INT | Upid of process involved in startup. | +| utid | INT | Utid of the thread. | +| thread_name | STRING | Name of the thread. | +| is_main_thread | BOOL | Thread is a main thread. | + +
+ +
+ +
+ +**android_thread_slices_for_all_startups**. All the slices for all startups in trace. Generally, this view should not be used + +
+ +VIEW +All the slices for all startups in trace. + +Generally, this view should not be used. Instead, use one of the view functions related +to the startup slices which are created from this table. + +| Column | Type | Description | +|---|---|---| +| startup_ts | INT | Timestamp of startup. | +| startup_ts_end | INT | Timestamp of startup end. | +| startup_id | INT | Startup id. | +| utid | INT | UTID of thread with slice. | +| thread_name | STRING | Name of thread. | +| is_main_thread | BOOL | Whether it is main thread. | +| arg_set_id | INT | Arg set id. | +| slice_id | INT | Slice id. | +| slice_name | STRING | Name of slice. | +| slice_ts | INT | Timestamp of slice start. | +| slice_dur | INT | Slice duration. | + +
+ +
+ +#### Functions + +
+ +**android_sum_dur_for_startup_and_slice** -\> INT. Returns duration of startup for slice name. Sums duration of all slices of startup with provided name. + +
+ +
+ +
+ +Returns duration of startup for slice name. + +Sums duration of all slices of startup with provided name. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +
+ +**android_sum_dur_on_main_thread_for_startup_and_slice** -\> INT. Returns duration of startup for slice name on main thread. Sums duration of all slices of startup with provided name only on main thread. + +
+ +
+ +
+ +Returns duration of startup for slice name on main thread. + +Sums duration of all slices of startup with provided name only on main thread. +Returns INT: Sum of duration. + +| Argument | Type | Description | +|---|---|---| +| startup_id | LONG | Startup id. | +| slice_name | STRING | Slice name. | + +
+ +
+ +#### Table Functions + +
+ +**android_slices_for_startup_and_slice_name** . Given a startup id and GLOB for a slice name, returns matching slices with data. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| slice_name | STRING | Glob of the slice. | + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Id of the slice. | +| slice_name | STRING | Name of the slice. | +| slice_ts | INT | Timestamp of start of the slice. | +| slice_dur | INT | Duration of the slice. | +| thread_name | STRING | Name of the thread with the slice. | +| arg_set_id | INT | Arg set id. | + +
+ +
+ +
+ +**android_binder_transaction_slices_for_startup** . Returns binder transaction slices for a given startup id with duration over threshold. + +
+ +
+ +
+ +| Argument | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| threshold | DOUBLE | Only return slices with duration over threshold. | + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| slice_dur | INT | Slice duration. | +| thread_name | STRING | Name of the thread with slice. | +| process | STRING | Name of the process with slice. | +| arg_set_id | INT | Arg set id. | +| is_main_thread | BOOL | Whether is main thread. | + +
+ +
+ +### android.startup.time_to_display + +#### Views/Tables + +
+ +**android_startup_time_to_display**. Startup metric defintions, which focus on the observable time range: TTID - Time To Initial Display \* https://developer.android.com/topic/performance/vitals/launch-time#time-initial \* end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display \* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD \* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. + +
+ +TABLE +Startup metric defintions, which focus on the observable time range: +TTID - Time To Initial Display +\* https://developer.android.com/topic/performance/vitals/launch-time#time-initial +\* end of first RenderThread.DrawFrame - bindApplication +TTFD - Time To Full Display +\* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD +\* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication +Googlers: see go/android-performance-metrics-glossary for details. + +| Column | Type | Description | +|---|---|---| +| startup_id | INT | Startup id. | +| time_to_initial_display | INT | Time to initial display (TTID) | +| time_to_full_display | INT | Time to full display (TTFD) | +| ttid_frame_id | INT | `android_frames.frame_id` of frame for initial display | +| ttfd_frame_id | INT | `android_frames.frame_id` of frame for full display | +| upid | INT | `process.upid` of the startup | + +
+ +
+ +### android.statsd + +#### Views/Tables + +
+ +**android_statsd_atoms**. Statsd atoms. A subset of the slice table containing statsd atom instant events. + +
+ +VIEW +Statsd atoms. + +A subset of the slice table containing statsd atom instant events. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique identifier for this slice. | +| type | STRING | The name of the "most-specific" child table containing this row. | +| ts | INT | The timestamp at the start of the slice (in nanoseconds). | +| dur | INT | The duration of the slice (in nanoseconds). | +| arg_set_id | INT | The id of the argument set associated with this slice. | +| thread_instruction_count | INT | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| thread_instruction_delta | INT | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | +| track_id | INT | The id of the track this slice is located on. | +| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | +| name | STRING | The name of the slice. The name describes what was happening during the slice. | +| depth | INT | The depth of the slice in the current stack of slices. | +| stack_id | INT | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | +| parent_stack_id | INT | The stack_id for the parent of this slice. Rarely useful. | +| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | +| thread_ts | INT | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | +| thread_dur | INT | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | + +
+ +
+ +### android.suspend + +#### Views/Tables + +
+ +**android_suspend_state**. Table of suspended and awake slices. Selects either the minimal or full ftrace source depending on what's available, marks suspended periods, and complements them to give awake periods. + +
+ +TABLE +Table of suspended and awake slices. + +Selects either the minimal or full ftrace source depending on what's +available, marks suspended periods, and complements them to give awake +periods. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Timestamp | +| dur | INT | Duration | +| power_state | STRING | 'awake' or 'suspended' | + +
+ +
+ +### android.winscope.inputmethod + +#### Views/Tables + +
+ +**android_inputmethod_clients**. Android inputmethod clients state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_manager_service**. Android inputmethod manager service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +
+ +**android_inputmethod_service**. Android inputmethod service state dumps (from android.inputmethod data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Dump id | +| ts | INT | Timestamp when the dump was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.viewcapture + +#### Views/Tables + +
+ +**android_viewcapture**. Android viewcapture (from android.viewcapture data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +### android.winscope.windowmanager + +#### Views/Tables + +
+ +**android_windowmanager**. Android WindowManager (from android.windowmanager data source). + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Snapshot id | +| ts | INT | Timestamp when the snapshot was triggered | +| arg_set_id | INT | Extra args parsed from the proto message | + +
+ +
+ +## Package: chrome + +### chrome.chrome_scrolls + +#### Views/Tables + +
+ +**chrome_scrolls**. Defines slices for all of the individual scrolls in a trace based on the LatencyInfo-based scroll definition. NOTE: this view of top level scrolls is based on the LatencyInfo definition of a scroll, which differs subtly from the definition based on EventLatencies. TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ WebView instances + +
+ +TABLE +Defines slices for all of the individual scrolls in a trace based on the +LatencyInfo-based scroll definition. + +NOTE: this view of top level scrolls is based on the LatencyInfo definition +of a scroll, which differs subtly from the definition based on +EventLatencies. +TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ +WebView instances. Currently gesture_scroll_id unique within an instance, but +is not unique across multiple instances. Switching to an EventLatency based +definition of scrolls should resolve this. + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the scroll. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| gesture_scroll_begin_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | +| gesture_scroll_end_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | + +
+ +
+ +### chrome.cpu_powerups + +#### Views/Tables + +
+ +**chrome_cpu_power_slice**. The CPU power transitions in the trace. Power states are encoded as non-negative integers, with zero representing full-power operation and positive values representing increasingly deep sleep states. On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep state that the CPU enters while idle. + +
+ +VIEW +The CPU power transitions in the trace. +Power states are encoded as non-negative integers, with zero representing +full-power operation and positive values representing increasingly deep +sleep states. + +On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep +state that the CPU enters while idle. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The CPU on which the transition occurred | +| power_state | INT | The power state that the CPU was in at time 'ts' for duration 'dur'. | +| previous_power_state | INT | The power state that the CPU was previously in. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_first_sched_slice_after_powerup**. The Linux scheduler slices that executed immediately after a CPU power up. + +
+ +TABLE +The Linux scheduler slices that executed immediately after a +CPU power up. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The timestamp at the start of the slice. | +| dur | INT | The duration of the slice. | +| cpu | INT | The cpu on which the slice executed. | +| sched_id | INT | Id for the sched_slice table. | +| utid | INT | Unique id for the thread that ran within the slice. | +| previous_power_state | INT | The CPU's power state before this slice. | +| powerup_id | INT | A unique ID for the CPU power-up. | + +
+ +
+ +
+ +**chrome_cpu_power_post_powerup_slice**. A table holding the slices that executed within the scheduler slice that ran on a CPU immediately after power-up. + +
+ +TABLE +A table holding the slices that executed within the scheduler +slice that ran on a CPU immediately after power-up. + +| Column | Type | Description | +|---|---|---| +| ts | None | Timestamp of the resulting slice | +| dur | None | Duration of the slice. | +| cpu | None | The CPU the sched slice ran on. | +| utid | None | Unique thread id for the slice. | +| sched_id | None | 'id' field from the sched_slice table. | +| type | None | From the sched_slice table, always 'sched_slice'. | +| end_state | None | The ending state for the sched_slice | +| priority | None | The kernel thread priority | +| slice_id | None | Id of the top-level slice for this (sched) slice. | + +
+ +
+ +
+ +**chrome_cpu_power_first_toplevel_slice_after_powerup**. The first top-level slice that ran after a CPU power-up. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | ID of the slice in the slice table. | +| previous_power_state | INT | The power state of the CPU prior to power-up. | + +
+ +
+ +### chrome.event_latency + +#### Views/Tables + +
+ +**chrome_event_latencies**. All EventLatency slices. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| is_presented | BOOL | Whether this input event was presented. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +
+ +**chrome_gesture_scroll_events**. All scroll-related events (frames) including gesture scroll updates, begins and ends with respective scroll ids and start/end timestamps, regardless of being presented + +
+ +TABLE +All scroll-related events (frames) including gesture scroll updates, begins +and ends with respective scroll ids and start/end timestamps, regardless of +being presented. This includes pinches that were presented. See b/315761896 +for context on pinches. + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice Id for the EventLatency scroll event. | +| name | STRING | Slice name. | +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | +| event_type | STRING | EventLatency event type. | +| track_id | INT | Perfetto track this slice is found on. | + +
+ +
+ +#### Functions + +
+ +**chrome_get_most_recent_scroll_begin_id** -\> INT. Extracts scroll id for the EventLatency slice at `ts`. + +
+ +
+ +
+ +Returns INT: The event_latency_id of the EventLatency slice with the type GESTURE_SCROLL_BEGIN that is the closest to `ts`. + +| Argument | Type | Description | +|---|---|---| +| ts | INT | Timestamp of the EventLatency slice to get the scroll id for. | + +
+ +
+ +### chrome.event_latency_description + +#### Views/Tables + +
+ +**chrome_event_latency_stage_descriptions**. Source of truth of the descriptions of EventLatency stages. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | A description of the EventLatency stage. | + +
+ +
+ +### chrome.histograms + +#### Views/Tables + +
+ +**chrome_histograms**. A helper view on top of the histogram events emitted by Chrome. Requires "disabled-by-default-histogram_samples" Chrome category. + +
+ +TABLE +A helper view on top of the histogram events emitted by Chrome. +Requires "disabled-by-default-histogram_samples" Chrome category. + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the histogram. | +| value | INT | The value of the histogram sample. | +| ts | INT | Alias of \|slice.ts\|. | +| thread_name | STRING | Thread name. | +| utid | INT | Utid of the thread. | +| tid | INT | Tid of the thread. | +| process_name | STRING | Process name. | +| upid | INT | Upid of the process. | +| pid | INT | Pid of the process. | + +
+ +
+ +### chrome.interactions + +#### Views/Tables + +
+ +**chrome_interactions**. All critical user interaction events, including type and table with associated metrics. + +
+ +TABLE +All critical user interaction events, including type and table with +associated metrics. + +| Column | Type | Description | +|---|---|---| +| scoped_id | INT | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | +| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | +| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | +| ts | INT | Timestamp of the CUI event. | +| dur | INT | Duration of the CUI event. | + +
+ +
+ +### chrome.metadata + +#### Functions + +
+ +**chrome_hardware_class** -\> STRING. Returns hardware class of the device, often use to find device brand and model. + +
+ +
+ +
+ +Returns hardware class of the device, often use to find device brand +and model. +Returns STRING: Hardware class name. + +
+ +
+ +### chrome.page_loads + +#### Views/Tables + +
+ +**chrome_page_loads**. Chrome page loads, including associated high-level metrics and properties. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | +| navigation_id | INT | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | +| navigation_start_ts | INT | Timestamp of the start of navigation. | +| fcp | INT | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | +| fcp_ts | INT | Timestamp of the first contentful paint. | +| lcp | INT | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | +| lcp_ts | INT | Timestamp of the largest contentful paint. | +| dom_content_loaded_event_ts | INT | Timestamp of the DomContentLoaded event: https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event | +| load_event_ts | INT | Timestamp of the window load event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event | +| mark_fully_loaded_ts | INT | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | +| mark_fully_visible_ts | INT | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | +| mark_interactive_ts | INT | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | +| url | STRING | URL at the page load event. | +| browser_upid | INT | The unique process id (upid) of the browser process where the page load occurred. | + +
+ +
+ +### chrome.scroll_interactions + +#### Views/Tables + +
+ +**chrome_scroll_interactions**. Top level scroll events, with metrics. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for an individual scroll. | +| name | STRING | Name of the scroll event. | +| ts | INT | Start timestamp of the scroll. | +| dur | INT | Duration of the scroll. | +| frame_count | INT | The total number of frames in the scroll. | +| vsync_count | INT | The total number of vsyncs in the scroll. | +| missed_vsync_max | INT | The maximum number of vsyncs missed during any and all janks. | +| missed_vsync_sum | INT | The total number of vsyncs missed during any and all janks. | +| delayed_frame_count | INT | The number of delayed frames. | +| predictor_janky_frame_count | INT | The number of frames that are deemed janky to the human eye after Chrome has applied its scroll prediction algorithm. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +### chrome.scroll_jank.predictor_error + +#### Views/Tables + +
+ +**chrome_predictor_error**. The scrolling offsets and predictor jank values for the actual (applied) scroll events. + +
+ +TABLE +The scrolling offsets and predictor jank values for the actual (applied) +scroll events. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| present_ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | +| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | +| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | +| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | +| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | +| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_map + +#### Views/Tables + +
+ +**chrome_scroll_jank_cause_descriptions**. Source of truth of the descriptions of EventLatency-based scroll jank causes. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| event_latency_stage | STRING | The name of the EventLatency stage. | +| cause_process | STRING | The process where the cause of scroll jank occurred. | +| cause_thread | STRING | The thread where the cause of scroll jank occurred. | +| cause_description | STRING | A description of the cause of scroll jank. | + +
+ +
+ +
+ +**chrome_scroll_jank_causes_with_event_latencies**. Combined description of scroll jank cause and associated event latency stage. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| name | STRING | The name of the EventLatency stage. | +| description | STRING | Description of the EventLatency stage. | +| cause_process | STRING | The process name that may cause scroll jank. | +| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | +| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_cause_utils + +#### Table Functions + +
+ +**chrome_select_scroll_jank_cause_thread** . Function to retrieve the thread id of the thread on a particular process if there are any slices during a particular EventLatency slice duration; this upid/thread combination refers to a cause of Scroll Jank. + +
+ +
+ +
+ +Function to retrieve the thread id of the thread on a particular process if +there are any slices during a particular EventLatency slice duration; this +upid/thread combination refers to a cause of Scroll Jank. +Argument \| Type \| Description +--- \| --- \| --- +event_latency_id \| INT \| The slice id of an EventLatency slice. +process_type \| STRING \| The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. +thread_name \| STRING \| The name of the thread. + +| Column | Type | Description | +|---|---|---| +| utid | INT | The utid associated with | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_intervals + +#### Views/Tables + +
+ +**chrome_janky_event_latencies_v3**. Selects EventLatency slices that correspond with janks in a scroll + +
+ +TABLE +Selects EventLatency slices that correspond with janks in a scroll. This is +based on the V3 version of scroll jank metrics. + +| Column | Type | Description | +|---|---|---| +| id | INT | The slice id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| track_id | INT | The track_id for the slice. | +| name | STRING | The name of the slice (EventLatency). | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| frame_jank_ts | INT | The start timestamp where frame presentation was delayed. | +| frame_jank_dur | INT | The duration in ms of the delay in frame presentation. | + +
+ +
+ +
+ +**chrome_janky_frame_presentation_intervals**. Frame presentation interval is the delta between when the frame was supposed to be presented and when it was actually presented. + +
+ +VIEW +Frame presentation interval is the delta between when the frame was supposed +to be presented and when it was actually presented. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id. | +| ts | INT | The start timestamp of the slice. | +| dur | INT | The duration of the slice. | +| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | +| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | +| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | +| event_latency_id | INT | The id of the associated event latency in the slice table. | + +
+ +
+ +
+ +**chrome_scroll_stats**. Scroll jank frame presentation stats for individual scrolls. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | Id of the individual scroll. | +| frame_count | INT | The number of frames in the scroll. | +| missed_vsyncs | INT | The number of missed vsyncs in the scroll. | +| presented_frame_count | INT | The number presented frames in the scroll. | +| janky_frame_count | INT | The number of janky frames in the scroll. | +| janky_frame_percent | FLOAT | The % of frames that janked in the scroll. | + +
+ +
+ +
+ +**chrome_scroll_jank_intervals_v3**. Defines slices for all of janky scrolling intervals in a trace. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | The unique identifier of the janky interval. | +| ts | INT | The start timestamp of the janky interval. | +| dur | INT | The duration of the janky interval. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3 + +#### Views/Tables + +
+ +**chrome_gesture_scroll_updates**. Grabs all gesture updates with respective scroll ids and start/end timestamps, regardless of being presented. + +
+ +TABLE +Grabs all gesture updates with respective scroll ids and start/end +timestamps, regardless of being presented. + +| Column | Type | Description | +|---|---|---| +| ts | INT | The start timestamp of the scroll. | +| dur | INT | The duration of the scroll. | +| id | INT | Slice id for the scroll. | +| scroll_update_id | INT | The id of the scroll update event. | +| scroll_id | INT | The id of the scroll. | +| is_presented | BOOL | Whether this input event was presented. | +| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_presented_gesture_scrolls**. Scroll updates, corresponding to all input events that were converted to a presented scroll update. + +
+ +TABLE +Scroll updates, corresponding to all input events that were converted to a +presented scroll update. + +| Column | Type | Description | +|---|---|---| +| id | INT | Minimum slice id for input presented in this frame, the non-presented input. | +| ts | INT | The start timestamp for producing the frame. | +| dur | INT | The duration between producing and presenting the frame. | +| last_presented_input_ts | INT | The timestamp of the last input that arrived and got presented in the frame. | +| scroll_update_id | INT | The id of the scroll update event, a unique identifier to the gesture. | +| scroll_id | INT | The id of the ongoing scroll. | +| presentation_timestamp | INT | Frame presentation timestamp. | +| event_type | STRING | EventLatency event type. | + +
+ +
+ +
+ +**chrome_scroll_updates_with_deltas**. Associate every trace_id with it's perceived delta_y on the screen after prediction. + +
+ +TABLE +Associate every trace_id with it's perceived delta_y on the screen after +prediction. + +| Column | Type | Description | +|---|---|---| +| scroll_update_id | INT | The id of the scroll update event. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | + +
+ +
+ +
+ +**chrome_full_frame_view**. Obtain the subset of input events that were fully presented. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_full_frame_delta_view**. Join deltas with EventLatency data. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| ts | INT | Start timestamp of the frame. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| last_presented_input_ts | INT | The timestamp of the last presented input. | +| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_merged_frame_view**. Group all gestures presented at the same timestamp together in a single row. + +
+ +TABLE +Group all gestures presented at the same timestamp together in +a single row. + +| Column | Type | Description | +|---|---|---| +| id | INT | ID of the frame. | +| max_start_ts | INT | The timestamp of the last presented input. | +| min_start_ts | INT | The earliest frame start timestamp. | +| scroll_id | INT | ID of the associated scroll. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | +| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | +| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | +| event_latency_id | INT | ID of the associated EventLatency. | +| dur | INT | Maximum duration of the associated EventLatency. | +| presentation_timestamp | INT | Frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_frame_info_with_delay**. View contains all chrome presented frames during gesture updates while calculating delay since last presented which usually should equal to \|VSYNC_INTERVAL\| if no jank is present. + +
+ +TABLE +View contains all chrome presented frames during gesture updates +while calculating delay since last presented which usually should +equal to \|VSYNC_INTERVAL\| if no jank is present. + +| Column | Type | Description | +|---|---|---| +| id | INT | gesture scroll slice id. | +| max_start_ts | INT | OS timestamp of the last touch move arrival within a frame. | +| min_start_ts | INT | OS timestamp of the first touch move arrival within a frame. | +| scroll_id | INT | The scroll which the touch belongs to. | +| scroll_update_id | INT | ID of the associated scroll update. | +| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | +| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | +| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives \|total_delta\|. | +| event_latency_id | INT | Event latency id of the presented frame. | +| dur | INT | Duration of the EventLatency. | +| presentation_timestamp | INT | Timestamp at which the frame was shown on the screen. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals \|VSYNC\| if no frame drops happened. | +| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_vsyncs**. Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the minimum delay between frames larger than zero. TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +
+ +TABLE +Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the +minimum delay between frames larger than zero. + +TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. + +| Column | Type | Description | +|---|---|---| +| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | + +
+ +
+ +
+ +**chrome_janky_frames_no_cause**. Filter the frame view only to frames that had missed vsyncs. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | + +
+ +
+ +
+ +**chrome_janky_frames_no_subcause**. Janky frame information including the jank cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | +| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | +| cause_id | INT | Id of the slice corresponding to the offending stage. | + +
+ +
+ +
+ +**chrome_janky_frames**. Finds all causes of jank for all janky frames, and a cause of sub jank if the cause of jank was GPU related. + +
+ +TABLE +Finds all causes of jank for all janky frames, and a cause of sub jank +if the cause of jank was GPU related. + +| Column | Type | Description | +|---|---|---| +| cause_of_jank | STRING | The reason the Vsync was missed. | +| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | +| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | +| event_latency_id | INT | Event latency id of the presented frame. | +| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | +| hardware_class | STRING | Device brand and model. | +| scroll_id | INT | The scroll corresponding to this frame. | + +
+ +
+ +
+ +**chrome_unique_frame_presentation_ts**. Counting all unique frame presentation timestamps. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| presentation_timestamp | INT | The unique frame presentation timestamp. | + +
+ +
+ +
+ +**chrome_janky_frames_percentage**. Dividing missed frames over total frames to get janky frame percentage. This represents the v3 scroll jank metrics. Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +
+ +TABLE +Dividing missed frames over total frames to get janky frame percentage. +This represents the v3 scroll jank metrics. +Reflects Event.Jank.DelayedFramesPercentage UMA metric. + +| Column | Type | Description | +|---|---|---| +| delayed_frame_percentage | FLOAT | The percent of missed frames relative to total frames - aka the percent of janky frames. | + +
+ +
+ +
+ +**chrome_frames_per_scroll**. Number of frames and janky frames per scroll. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| num_frames | INT | The number of frames in the scroll. | +| num_janky_frames | INT | The number of delayed/janky frames. | +| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | + +
+ +
+ +
+ +**chrome_causes_per_scroll**. Scroll jank causes per scroll. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | The ID of the scroll. | +| max_delay_since_last_frame | DOUBLE | The maximum time a frame was delayed after the presentation of the previous frame. | +| vsync_interval | DOUBLE | The expected vsync interval. | +| scroll_jank_causes | BYTES | A proto amalgamation of each scroll jank cause including cause name, sub cause and the duration of the delay since the previous frame was presented. | + +
+ +
+ +### chrome.scroll_jank.scroll_jank_v3_cause + +#### Functions + +
+ +**chrome_get_v3_jank_cause_id** -\> LONG. Given two slice Ids A and B, find the maximum difference between the durations of it's direct children with matching names for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) and slice B has children named (X, Y) with durations of (9, 9), the function will return the slice id of the slice named Z that is A's child, as no matching slice named Z was found under B, making 5 - 0 = 5 the maximum delta between both slice's direct children + +
+ +
+ +
+ +Given two slice Ids A and B, find the maximum difference +between the durations of it's direct children with matching names +for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) +and slice B has children named (X, Y) with durations of (9, 9), the function will return +the slice id of the slice named Z that is A's child, as no matching slice named Z was found +under B, making 5 - 0 = 5 the maximum delta between both slice's direct children +Returns LONG: The slice id of the breakdown that has the maximum duration delta. + +| Argument | Type | Description | +|---|---|---| +| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | +| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to \|janky_slice_id\|. | + +
+ +
+ +### chrome.scroll_jank.scroll_offsets + +#### Views/Tables + +
+ +**chrome_scroll_input_offsets**. The raw coordinates and pixel offsets for all input events which were part of a scroll. + +
+ +TABLE +The raw coordinates and pixel offsets for all input events which were part of +a scroll. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Timestamp the of the scroll input event. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +
+ +**chrome_presented_scroll_offsets**. The scrolling offsets for the actual (applied) scroll events + +
+ +TABLE +The scrolling offsets for the actual (applied) scroll events. These are not +necessarily inclusive of all user scroll events, rather those scroll events +that are actually processed. + +| Column | Type | Description | +|---|---|---| +| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | +| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | +| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | +| ts | INT | Presentation timestamp. | +| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | +| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | + +
+ +
+ +### chrome.scroll_jank.utils + +#### Table Functions + +
+ +**chrome_select_long_task_slices** . Extract mojo information for the long-task-tracking scenario for specific names + +
+ +
+ +
+ +Extract mojo information for the long-task-tracking scenario for specific +names. For example, LongTaskTracker slices may have associated IPC +metadata, or InterestingTask slices for input may have associated IPC to +determine whether the task is fling/etc. +Argument \| Type \| Description +--- \| --- \| --- +name \| STRING \| The name of slice. + +| Column | Type | Description | +|---|---|---| +| interface_name | STRING | Name of the interface of the IPC call. | +| ipc_hash | INT | Hash of the IPC call. | +| message_type | STRING | Message type (e.g. reply). | +| id | INT | The slice id. | + +
+ +
+ +### chrome.speedometer + +#### Views/Tables + +
+ +**chrome_speedometer_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_iteration**. Slice that covers one Speedometer iteration. Depending on the Speedometer version these slices might need to be estimated as older versions of Speedometer to not emit marks for this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +Depending on the Speedometer version these slices might need to be estimated +as older versions of Speedometer to not emit marks for this interval. The +metrics associated are the same ones Speedometer would output, but note we +use ns precision (Speedometer uses \~100us) so the actual values might differ +a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_score** -\> DOUBLE. Returns the Speedometer score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer score + +
+ +
+ +
+ +**chrome_speedometer_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_2_1 + +#### Views/Tables + +
+ +**chrome_speedometer_2_1_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async sync is the time between the start and sync-end marks, async is the time between the sync-end and async-end marks. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async +sync is the time between the start and sync-end marks, async is the time +between the sync-end and async-end marks. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_2_1_iteration**. Slice that covers one Speedometer iteration. This slice is actually estimated as a default Speedometer run will not emit marks to cover this interval + +
+ +TABLE +Slice that covers one Speedometer iteration. +This slice is actually estimated as a default Speedometer run will not emit +marks to cover this interval. The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. Also note Speedometer +returns the values in ms these here and in ns. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_2_1_score** -\> DOUBLE. Returns the Speedometer 2.1 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 2.1 score + +
+ +
+ +
+ +**chrome_speedometer_2_1_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 2.1 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.speedometer_3 + +#### Views/Tables + +
+ +**chrome_speedometer_3_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async. + +
+ +TABLE +Augmented slices for Speedometer measurements. +These are the intervals of time Speedometer uses to compute the final score. +There are two intervals that are measured for every test: sync and async. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the measure slice | +| dur | INT | Duration of the measure slice | +| name | STRING | Full measure name | +| iteration | INT | Speedometer iteration the slice belongs to. | +| suite_name | STRING | Suite name | +| test_name | STRING | Test name | +| measure_type | STRING | Type of the measure (sync or async) | + +
+ +
+ +
+ +**chrome_speedometer_3_iteration**. Slice that covers one Speedometer iteration. The metrics associated are the same ones Speedometer would output, but note we use ns precision (Speedometer uses \~100us) so the actual values might differ a bit. + +
+ +TABLE +Slice that covers one Speedometer iteration. +The metrics associated are the same ones +Speedometer would output, but note we use ns precision (Speedometer uses +\~100us) so the actual values might differ a bit. + +| Column | Type | Description | +|---|---|---| +| ts | INT | Start timestamp of the iteration | +| dur | INT | Duration of the iteration | +| name | STRING | Iteration name | +| iteration | INT | Iteration number | +| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | +| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | + +
+ +
+ +#### Functions + +
+ +**chrome_speedometer_3_score** -\> DOUBLE. Returns the Speedometer 3 score for all iterations in the trace + +
+ +
+ +
+ +Returns DOUBLE: Speedometer 3 score + +
+ +
+ +
+ +**chrome_speedometer_3_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 + +
+ +
+ +
+ +Returns INT: Renderer main utid + +
+ +
+ +### chrome.startups + +#### Views/Tables + +
+ +**chrome_startups**. Chrome startups, including launch cause. + +
+ +TABLE + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique ID | +| activity_id | INT | Chrome Activity event id of the launch. | +| name | STRING | Name of the launch start event. | +| startup_begin_ts | INT | Timestamp that the startup occurred. | +| first_visible_content_ts | INT | Timestamp to the first visible content. | +| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | +| browser_upid | INT | Process ID of the Browser where the startup occurred. | + +
+ +
+ +### chrome.tasks + +#### Views/Tables + +
+ +**chrome_java_views**. A list of slices corresponding to operations on interesting (non-generic) Chrome Java views + +
+ +VIEW +A list of slices corresponding to operations on interesting (non-generic) +Chrome Java views. The view is considered interested if it's not a system +(ContentFrameLayout) or generic library (CompositorViewHolder) views. + +TODO(altimin): Add "columns_from slice" annotation. +TODO(altimin): convert this to EXTEND_TABLE when it becomes available. + +| Column | Type | Description | +|---|---|---| +| filtered_name | STRING | Name of the view. | +| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | +| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | +| slice_id | INT | Slice id. | + +
+ +
+ +
+ +**chrome_scheduler_tasks**. A list of tasks executed by Chrome scheduler. + +
+ +VIEW + +| Column | Type | Description | +|---|---|---| +| id | INT | Slice id. | +| type | STRING | Type. | +| name | STRING | Name of the task. | +| ts | INT | Timestamp. | +| dur | INT | Duration. | +| utid | INT | Utid of the thread this task run on. | +| thread_name | STRING | Name of the thread this task run on. | +| upid | INT | Upid of the process of this task. | +| process_name | STRING | Name of the process of this task. | +| track_id | INT | Same as slice.track_id. | +| category | STRING | Same as slice.category. | +| depth | INT | Same as slice.depth. | +| parent_id | INT | Same as slice.parent_id. | +| arg_set_id | INT | Same as slice.arg_set_id. | +| thread_ts | INT | Same as slice.thread_ts. | +| thread_dur | INT | Same as slice.thread_dur. | +| posted_from | STRING | Source location where the PostTask was called. | + +
+ +
+ +
+ +**chrome_tasks**. A list of "Chrome tasks": top-level execution units (e.g + +
+ +VIEW +A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / +IPCs / system callbacks) run by Chrome. For a given thread, the slices +corresponding to these tasks will not intersect. + +| Column | Type | Description | +|---|---|---| +| id | INT | Id for the given task, also the id of the slice this task corresponds to. | +| name | STRING | Name for the given task. | +| task_type | STRING | Type of the task (e.g. "scheduler"). | +| thread_name | STRING | Thread name. | +| utid | INT | Utid. | +| process_name | STRING | Process name. | +| upid | INT | Upid. | +| ts | INT | Alias of \|slice.ts\|. | +| dur | INT | Alias of \|slice.dur\|. | +| track_id | INT | Alias of \|slice.track_id\|. | +| category | STRING | Alias of \|slice.category\|. | +| arg_set_id | INT | Alias of \|slice.arg_set_id\|. | +| thread_ts | INT | Alias of \|slice.thread_ts\|. | +| thread_dur | INT | Alias of \|slice.thread_dur\|. | +| full_name | STRING | STRING Legacy alias for \|name\|. | + +
+ +
+ +### chrome.vsync_intervals + +#### Views/Tables + +
+ +**chrome_vsync_intervals**. A simple table that checks the time between VSync (this can be used to determine if we're refreshing at 90 FPS or 60 FPS). Note: In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +
+ +TABLE +A simple table that checks the time between VSync (this can be used to +determine if we're refreshing at 90 FPS or 60 FPS). + +> [!NOTE] +> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. + +| Column | Type | Description | +|---|---|---| +| slice_id | INT | Slice id of the vsync slice. | +| ts | INT | Timestamp of the vsync slice. | +| dur | INT | Duration of the vsync slice. | +| track_id | INT | Track id of the vsync slice. | +| time_to_next_vsync | INT | Duration until next vsync arrives. | + +
+ +
+ +#### Functions + +
+ +**chrome_calculate_avg_vsync_interval** -\> FLOAT. Function: compute the average Vysnc interval of the gesture (hopefully this would be either 60 FPS for the whole gesture or 90 FPS but that isnt always the case) on the given time segment. If the trace doesnt contain the VSync TraceEvent we just fall back on assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which corresponds to 16 ms or 60 FPS). + +
+ +
+ +
+ +Function: compute the average Vysnc interval of the +gesture (hopefully this would be either 60 FPS for the whole gesture or 90 +FPS but that isnt always the case) on the given time segment. +If the trace doesnt contain the VSync TraceEvent we just fall back on +assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which +corresponds to 16 ms or 60 FPS). +Returns FLOAT: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. + +| Argument | Type | Description | +|---|---|---| +| begin_ts | LONG | Interval start time. | +| end_ts | LONG | Interval end time. | + +
+ +
+ +### chrome.web_content_interactions + +#### Views/Tables + +
+ +**chrome_web_content_interactions**. Chrome web content interactions (InteractionToFirstPaint), including associated high-level metrics and properties. Multiple events may occur for the same interaction; each row in this table represents the primary (longest) event for the interaction. Web content interactions are discrete, as opposed to sustained (e.g. scrolling); and only occur with the web content itself, as opposed to other parts of Chrome (e.g + +
+ +TABLE +Chrome web content interactions (InteractionToFirstPaint), including +associated high-level metrics and properties. + +Multiple events may occur for the same interaction; each row in this table +represents the primary (longest) event for the interaction. + +Web content interactions are discrete, as opposed to sustained (e.g. +scrolling); and only occur with the web content itself, as opposed to other +parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, +keyboard input (typing), and drags. + +| Column | Type | Description | +|---|---|---| +| id | INT | Unique id for this interaction. | +| ts | INT | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | +| dur | INT | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | +| interaction_type | STRING | The interaction type. | +| total_duration_ms | INT | The total duration of all events that occurred for the same interaction. | +| renderer_upid | INT | The process id this event occurred on. | + +
+ +
+ +## Package: counters + +### counters.intervals + +#### Macros + +
+ +**counter_leading_intervals** . For a given counter timeline (e.g + +
+ +
+ +
+ +For a given counter timeline (e.g. a single counter track), returns +intervals of time where the counter has the same value. + +Intervals are computed in a "forward-looking" way. That is, if a counter +changes value at some timestamp, it's assumed it *just* reached that +value and it should continue to have that value until the next +value change. The final value is assumed to hold until the very end of +the trace. + +For example, suppose we have the following data: +`ts=0, value=10, track_id=1 +ts=0, value=10, track_id=2 +ts=10, value=10, track_id=1 +ts=10, value=20, track_id=2 +ts=20, value=30, track_id=1 +[end of trace at ts = 40]` + +Then this macro will generate the following intervals: +`ts=0, dur=20, value=10, track_id=1 +ts=20, dur=10, value=30, track_id=1 +ts=0, dur=10, value=10, track_id=2 +ts=10, dur=30, value=20, track_id=2` +Returns: TableOrSubquery, Table with the schema (id UINT32, ts UINT64, dur UINT64, track_id UINT64, value DOUBLE, next_value DOUBLE, delta_value DOUBLE). + +| Argument | Type | Description | +|---|---|---| +| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | + +
+ +
+ +## Package: graphs + +### graphs.dominator_tree + +#### Macros + +
+ +**graph_dominator_tree** . Given a table containing a directed flow-graph and an entry node, computes the "dominator tree" for the graph + +
+ +
+ +
+ +Given a table containing a directed flow-graph and an entry node, computes +the "dominator tree" for the graph. See \[1\] for an explanation of what a +dominator tree is. + +\[1\] https://en.wikipedia.org/wiki/Dominator_(graph_theory) + +Example usage on traces containing heap graphs: +\`\`\` +CREATE PERFETTO VIEW dominator_compatible_heap_graph AS +-- Extract the edges from the heap graph which correspond to references +-- between objects. +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +JOIN heap_graph_object owner on heap_graph_reference.owner_id = owner.id +WHERE owned_id IS NOT NULL AND owner.reachable +UNION ALL +-- Since a Java heap graph is a "forest" structure, we need to add a dummy +-- "root" node which connects all the roots of the forest into a single +-- connected component. +SELECT +(SELECT max(id) + 1 FROM heap_graph_object) as source_node_id, +id +FROM heap_graph_object +WHERE root_type IS NOT NULL; + +SELECT \* +FROM graph_dominator_tree!( +dominator_compatible_heap_graph, +(SELECT max(id) + 1 FROM heap_graph_object) +); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, dominator_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with \|root_node_id\| (see below) being the "entry node" for this component. Specifically, all nodes *must* be reachable by following paths from the root node. Failing to adhere to this property will result in undefined behaviour. If working with a "forest"-like structure, a dummy node should be added which links all the roots of the forest together into a single component; an example of this can be found in the heap graph example query above. | +| root_node_id | Expr | The entry node to \|graph_table\| which will be the root of the dominator tree. | + +
+ +
+ +### graphs.partition + +#### Macros + +
+ +**tree_structural_partition_by_group** . Partitions a tree into a forest of trees based on a given grouping key in a structure-preserving way. Specifically, for each tree in the output forest, all the nodes in that tree have the same ancestors and descendants as in the original tree *iff* that ancestor/descendent belonged to the same group. Example: Input id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 4 \| 1 Or as a graph: `1 (1) / 2 (1) / \ 3 (2) 4 (2) / \ 5 (1) 8 (1) / \ 6 (3) 7 (2)` Possible output (order of rows is implementation-defined) id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 2 \| 1 Or as a forest: `1 (1) 3 (2) 4 (2) 6 (3) | | 2 (1) 7 (2) / \ 5 (1) 8 (1)` + +
+ +
+ +
+ +Partitions a tree into a forest of trees based on a given grouping key +in a structure-preserving way. + +Specifically, for each tree in the output forest, all the nodes in that tree +have the same ancestors and descendants as in the original tree *iff* that +ancestor/descendent belonged to the same group. + +Example: +Input + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 4 | 1 | + +Or as a graph: +`1 (1) +/ +2 (1) +/ \ +3 (2) 4 (2) +/ \ +5 (1) 8 (1) +/ \ +6 (3) 7 (2)` +Possible output (order of rows is implementation-defined) + +| id | parent_id | group_key | +|---|---|---| +| 1 | NULL | 1 | +| 2 | 1 | 1 | +| 3 | NULL | 2 | +| 4 | NULL | 2 | +| 5 | 2 | 1 | +| 6 | NULL | 3 | +| 7 | 4 | 2 | +| 8 | 2 | 1 | + +Or as a forest: +`1 (1) 3 (2) 4 (2) 6 (3) +| | +2 (1) 7 (2) +/ \ +5 (1) 8 (1)` +Returns: TableOrSubquery, The returned table has the schema (id UINT32, parent_id UINT32, group_key UINT32). + +| Argument | Type | Description | +|---|---|---| +| tree_table | TableOrSubquery | A table/view/subquery corresponding to a tree which should be partitioned. This table must have the columns "id", "parent_id" and "group_key". Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | + +
+ +
+ +### graphs.search + +#### Macros + +
+ +**graph_reachable_dfs** . Computes the "reachable" set of nodes in a directed graph from a given set of starting nodes by performing a depth-first search on the graph + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a given set +of starting nodes by performing a depth-first search on the graph. The +returned nodes are structured as a tree with parent-child relationships +corresponding to the order in which nodes were encountered by the DFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: +`-- Compute the reachable nodes from the first heap root. +SELECT * +FROM graph_reachable_dfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +
+ +
+ +
+ +**graph_reachable_bfs** . Computes the "reachable" set of nodes in a directed graph from a given starting node by performing a breadth-first search on the graph + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a given +starting node by performing a breadth-first search on the graph. The returned +nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the BFS. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces containing heap graphs: +`-- Compute the reachable nodes from all heap roots. +SELECT * +FROM graph_reachable_bfs!( +( +SELECT +owner_id AS source_node_id, +owned_id as dest_node_id +FROM heap_graph_reference +WHERE owned_id IS NOT NULL +), +(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | + +
+ +
+ +
+ +**graph_next_sibling** . Computes the next sibling node in a directed graph + +
+ +
+ +
+ +Computes the next sibling node in a directed graph. The next node under a parent node +is determined by on the \|sort_key\|, which should be unique for every node under a parent. +The order of the next sibling is undefined if the \|sort_key\| is not unique. + +Example usage: +`-- Compute the next sibling: +SELECT * +FROM graph_next_sibling!( +( +SELECT +id AS node_id, +parent_id AS node_parent_id, +ts AS sort_key +FROM slice +) +);` +Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, next_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph for which to find the next sibling. This table must have the columns "node_id", "node_parent_id" and "sort_key". | + +
+ +
+ +
+ +**graph_reachable_weight_bounded_dfs** . Computes the "reachable" set of nodes in a directed graph from a set of starting (root) nodes by performing a depth-first search from each root node on the graph. The search is bounded by the sum of edge weights on the path and the root node specifies the max weight (inclusive) allowed before stopping the search. The returned nodes are structured as a tree with parent-child relationships corresponding to the order in which nodes were encountered by the DFS + +
+ +
+ +
+ +Computes the "reachable" set of nodes in a directed graph from a set of +starting (root) nodes by performing a depth-first search from each root node on the graph. +The search is bounded by the sum of edge weights on the path and the root node specifies the +max weight (inclusive) allowed before stopping the search. +The returned nodes are structured as a tree with parent-child relationships corresponding +to the order in which nodes were encountered by the DFS. Each row also has the root node from +which where the edge was encountered. + +While this macro can be used directly by end users (hence being public), +it is primarily intended as a lower-level building block upon which higher +level functions/macros in the standard library can be built. + +Example usage on traces with sched info: +\`\`\` +-- Compute the reachable nodes from a sched wakeup chain +INCLUDE PERFETTO MODULE sched.thread_executing_spans; + +SELECT \* +FROM +graph_reachable_dfs_bounded +!( +( +SELECT +id AS source_node_id, +COALESCE(parent_id, id) AS dest_node_id, +id - COALESCE(parent_id, id) AS edge_weight +FROM _wakeup_chain +), +( +SELECT +id AS root_node_id, +id - COALESCE(prev_id, id) AS root_target_weight +FROM _wakeup_chain +)); +\`\`\` +Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. + +| Argument | Type | Description | +|---|---|---| +| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph and an "edge_weight" corresponding to the weight of the edge between the node. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to \|graph_table\| which will be the roots of the reachability trees. This table must have the columns "root_node_id" and "root_target_weight" corresponding to the starting node id and the max weight allowed on the tree. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | +| is_target_weight_floor | Expr | Whether the target_weight is a floor weight or ceiling weight. If it's floor, the search stops right after we exceed the target weight, and we include the node that pushed just passed the target. If ceiling, the search stops right before the target weight and the node that would have pushed us passed the target is not included. | + +
+ +
+ +## Package: export + +### export.to_firefox_profile + +#### Functions + +
+ +**export_to_firefox_profile** -\> STRING. Dumps all trace data as a Firefox profile json string See `Profile` in https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js Also https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html You would probably want to download the generated json and then open at https://https://profiler.firefox.com You can easily do this from the UI via the following SQL `SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` The result will have a link for you to download this json as a file. + +
+ +
+ +
+ +Dumps all trace data as a Firefox profile json string +See `Profile` in +https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js +Also +https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html + +You would probably want to download the generated json and then open at +https://https://profiler.firefox.com +You can easily do this from the UI via the following SQL +`SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` +The result will have a link for you to download this json as a file. +Returns STRING: Json profile + +
+ +
\ No newline at end of file diff --git a/profilers/perfetto-trace-analysis/references/sql.md b/profilers/perfetto-trace-analysis/references/sql.md index 8ec1baa..6f59208 100644 --- a/profilers/perfetto-trace-analysis/references/sql.md +++ b/profilers/perfetto-trace-analysis/references/sql.md @@ -60,7 +60,7 @@ ## Resources -- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/common/perfetto-stdlib). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. +- **Documentation:** The Perfetto Standard Library documentation is in [`perfetto-stdlib.md`](perfetto-stdlib.md). Use this file as a reference to discover available modules, find schemas (columns and types) for specific tables or views, or determine the `INCLUDE PERFETTO MODULE` statements required before drafting SQL query. - **Execution Tool:** Queries are executed using the official `trace_processor` wrapper script downloaded directly from Perfetto. Output is returned in pure CSV format. ## Execution Protocol @@ -82,7 +82,7 @@ working directory or in your path, download it directly from the Perfetto index: 1. Identify the core question, required data points, and filtering conditions. 2. **Precedence Rule:** If the user's request contains a SQL query, use it **without modification** and skip to Step 2 for validation. -3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`references/perfetto-stdlib-docs.md`](https://developer.android.com/agents/skills/profilers/common/perfetto-stdlib). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: +3. **Mandatory Schema and Module Search:** For every table or view you plan to use, you MUST find its schema in [`perfetto-stdlib.md`](perfetto-stdlib.md). **Don't read the entire documentation file** --- it consumes the context window. Follow this precise workflow: - **Discovery and Search:** Use available search tools (`grep`, `read_file` or file search) with line limits to discover relevant views, tables or modules based on your problem domain and high-level intents (for example, 'CPU time', 'running time', 'overlap', 'jank'). - **Why:** Searching solely for exact table names misses comprehensive, pre-computed views built for these analyses. - **Note:** You must verify if a Standard Library module already provides the needed abstraction before drafting manual arithmetic or custom functions. From 8f61cf7b17998d63b4fb083e54943ad178bb2d88 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 15 May 2026 17:58:44 +0000 Subject: [PATCH 18/61] Updates skills (2026-05-15 17:58) --- profilers/perfetto-sql/SKILL.md | 15 +++++++-------- profilers/perfetto-trace-analysis/SKILL.md | 11 ++++++----- .../perfetto-trace-analysis/references/sql.md | 15 +++++++-------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/profilers/perfetto-sql/SKILL.md b/profilers/perfetto-sql/SKILL.md index 20712b0..430b3ad 100644 --- a/profilers/perfetto-sql/SKILL.md +++ b/profilers/perfetto-sql/SKILL.md @@ -89,14 +89,13 @@ You must follow these steps sequentially, mirroring a multi-agent pipeline: ### Step 0: Tool Setup -**Fetch the Wrapper:** If you don't have `trace_processor` in your current -working directory or in your path, download it directly from the Perfetto index: -`curl -LO https://get.perfetto.dev/trace_processor && chmod +x -./trace_processor` -> **Important:** The file served at this URL is a `~10KB` Python wrapper script. -> Don't assume the download failed because it is human-readable text. This is -> the intended behavior. This script handles lazy-loading the real binary into -> `~/.local/share/perfetto/` on its first run. Use it directly. +**Fetch the Wrapper:** You must use the top level of the current project workspace (`./trace_processor`). + +> **CRITICAL GUARDRAIL:** NEVER use filesystem search tools (`find`, `find_by_name`, `grep`, `dir /s`, `Get-ChildItem`) across the home directory or workspace to locate `trace_processor` — unconstrained searches across entire workspaces will stop responding or time out. + +Perform a direct file check at the top level of your workspace (e.g., `ls trace_processor`). If missing, download `https://get.perfetto.dev/trace_processor` directly into the root workspace (`curl -LO`), make it executable on macOS/Linux (`chmod +x`), and ensure `trace_processor` is added to `.gitignore`. Execute queries directly via `./trace_processor` (on Windows, explicitly invoke `python trace_processor`). + +> **Important:** The file served at this URL is a `~10KB` Python wrapper script. Don't assume the download failed because it is human-readable text. This is the intended behavior. This script handles lazy-loading the precompiled binary automatically on its first run. Use it directly. ### Step 1: Dissection and Schema Research diff --git a/profilers/perfetto-trace-analysis/SKILL.md b/profilers/perfetto-trace-analysis/SKILL.md index 9e50747..c74a217 100644 --- a/profilers/perfetto-trace-analysis/SKILL.md +++ b/profilers/perfetto-trace-analysis/SKILL.md @@ -20,7 +20,7 @@ metadata: ## Resources -- **Domain Hints:** Reference files for specific performance areas: [`CPU`](references/hints_cpu.md), [`Graphics`](references/hints_graphics.md), [`I/O`](references/hints_io.md), [`IPC`](references/hints_ipc.md), [`Memory`](references/hints_memory.md), [`Power`](references/hints_power.md). +- **Domain Hints:** Reference files for specific performance areas: [`CPU`](references/hints_cpu.md), [`Graphics`](references/hints_graphics.md), [`I/O`](references/hints_io.md), [`IPC`](references/hints_ipc.md), [`Memory`](references/hints_memory.md), [`Power`](references/hints_power.md). These files each contain multiple expert-vetted, powerful trace analysis techniques to steer and aid in the analysis. - **Perfetto SQL Reference:** Reference guidelines for translating intents into valid queries are located in [the SQL reference](references/sql.md). You must read this reference and follow its Execution Protocol for all SQL generation. ## Setup Phase (Mandatory) @@ -30,8 +30,9 @@ metadata: - Name the file using the trace's filename appended with `_analysis.md` (e.g., `[trace_filename]_analysis.md`). Before creating it, check if a file with that name already exists by listing the directory's contents---to avoid biasing your investigation, DO NOT read the file's contents to check for its existence. If it does, append an incrementing version number (e.g., `_v2.md`, `_v3.md`) until you find an available filename. You MUST hardcode this exact filename in all subsequent tool calls. - Use this scratchpad STRICTLY to log verified facts: timestamps, slice names, thread IDs (utid/tid), and thread states. - DO NOT write preliminary hypotheses or premature conclusions in the scratchpad. It is a strict Chain of Evidence. -2. **Review SQL Reference:** Read the SQL reference in [`references/sql.md`](references/sql.md) and follow its Execution Protocol for all SQL generation. Do not guess schemas. -3. **Target Resolution:** If the user's request is broad (e.g., "why is the app slow?") and doesn't specify a package name: +2. **Review Domain Hints:** Read the Domain Hints in each file to get a high-level overview of what techniques are possible. Make sure to use this baseline knowledge when researching and retrieving hints during the ongoing investigation. +3. **Review SQL Reference:** Read the SQL reference in [`references/sql.md`](references/sql.md) and follow its Execution Protocol for all SQL generation. Do not guess schemas. +4. **Target Resolution:** If the user's request is broad (e.g., "why is the app slow?") and doesn't specify a package name: - Execute a query to identify the active application: `sql INCLUDE PERFETTO MODULE android.startup.startups; SELECT package FROM android_startups;` @@ -43,7 +44,7 @@ Follow this iterative loop until you have isolated the definitive root cause(s): ### 1. Formulate Hypothesis -- **Prioritization:** Form hypotheses using information from: user prompt \> built-in hints ([`references/hints_*.md`](references/hints_cpu.md)) \> general knowledge. +- **Prioritization:** Form hypotheses using information from: user prompt \> "Domain Hints" ([`CPU`](references/hints_cpu.md), [`Graphics`](references/hints_graphics.md), [`I/O`](references/hints_io.md), [`IPC`](references/hints_ipc.md), [`Memory`](references/hints_memory.md), [`Power`](references/hints_power.md)) \> general knowledge. Be sure to leverage these "Domain Hints" as they are expert-vetted analysis techniques. - **Source Attribution:** Explicitly mention the source of your hypothesis (e.g., "Based on hints_io.md..."). - **Focus Constraint:** Focus on the primary bottleneck. Avoid investigating deep into binder transactions unless the user explicitly asks for it or there is no other obvious bottleneck. - **State Reasoning:** Briefly state your reasoning based on previous findings *before* generating a new query. @@ -63,7 +64,7 @@ Follow this iterative loop until you have isolated the definitive root cause(s): ### 4. Exhaustive Investigation (Do Not Give Up Early) -- **Multiple Bottlenecks:** Complex performance issues rarely have a single cause. Do NOT stop your investigation after finding the first anomaly. Even if you find a major bottleneck (e.g., emulator graphics lag), you MUST continue searching for other independent system-wide issues (e.g., lock contention, I/O stalls). +- **Multiple Bottlenecks:** Complex performance issues rarely have a single cause. Do NOT stop your investigation after finding the first anomaly. Even if you find a major bottleneck (e.g., emulator graphics lag), you MUST continue searching for other independent system-wide issues (e.g., lock contention, I/O stalls). To find other bottlenecks, search through the content of the "Domain Hints" files ([`CPU`](references/hints_cpu.md), [`Graphics`](references/hints_graphics.md), [`I/O`](references/hints_io.md), [`IPC`](references/hints_ipc.md), [`Memory`](references/hints_memory.md), [`Power`](references/hints_power.md)) to retrieve and leverage expert-vetted, powerful trace analysis techniques. Investigate each relevant hint with depth. - **Global Verification:** Periodically perform a system-wide query for the longest running slices (`ORDER BY slice.dur DESC`) and most frequent D-states to ensure your local investigation hasn't missed a massive, unrelated system stall. - **Persist Through Dead Ends:** If a hypothesis is disproven or a query returns empty, do not conclude. Pivot your focus, broaden your search constraints (fuzzy matching, wider time windows), and continue the mission. diff --git a/profilers/perfetto-trace-analysis/references/sql.md b/profilers/perfetto-trace-analysis/references/sql.md index 6f59208..49a07f2 100644 --- a/profilers/perfetto-trace-analysis/references/sql.md +++ b/profilers/perfetto-trace-analysis/references/sql.md @@ -69,14 +69,13 @@ You must follow these steps sequentially, mirroring a multi-agent pipeline: ### Step 0: Tool Setup -**Fetch the Wrapper:** If you don't have `trace_processor` in your current -working directory or in your path, download it directly from the Perfetto index: -`curl -LO https://get.perfetto.dev/trace_processor && chmod +x -./trace_processor` -> **Important:** The file served at this URL is a `~10KB` Python wrapper script. -> Don't assume the download failed because it is human-readable text. This is -> the intended behavior. This script handles lazy-loading the real binary into -> `~/.local/share/perfetto/` on its first run. Use it directly. +**Fetch the Wrapper:** You must use the top level of the current project workspace (`./trace_processor`). + +> **CRITICAL GUARDRAIL:** NEVER use filesystem search tools (`find`, `find_by_name`, `grep`, `dir /s`, `Get-ChildItem`) across the home directory or workspace to locate `trace_processor` — unconstrained searches across entire workspaces will stop responding or time out. + +Perform a direct file check at the top level of your workspace (e.g., `ls trace_processor`). If missing, download `https://get.perfetto.dev/trace_processor` directly into the root workspace (`curl -LO`), make it executable on macOS/Linux (`chmod +x`), and ensure `trace_processor` is added to `.gitignore`. Execute queries directly via `./trace_processor` (on Windows, explicitly invoke `python trace_processor`). + +> **Important:** The file served at this URL is a `~10KB` Python wrapper script. Don't assume the download failed because it is human-readable text. This is the intended behavior. This script handles lazy-loading the precompiled binary automatically on its first run. Use it directly. ### Step 1: Dissection and Schema Research From e1c91d1c16c79f0ab39264c420169dedb6db6fb3 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 15 May 2026 18:41:43 +0000 Subject: [PATCH 19/61] Updates skills (2026-05-15 18:41) --- jetpack-compose/adaptive/SKILL.md | 301 ++++ .../adaptive/flexbox/container-behavior.md | 112 ++ .../layouts/adaptive/flexbox/get-started.md | 69 + .../compose/layouts/adaptive/flexbox/index.md | 81 ++ .../layouts/adaptive/flexbox/item-behavior.md | 170 +++ .../adaptive/grid/container-properties.md | 238 ++++ .../layouts/adaptive/grid/get-started.md | 51 + .../ui/compose/layouts/adaptive/grid/index.md | 73 + .../layouts/adaptive/grid/item-properties.md | 168 +++ .../layouts/adaptive/mediaquery/index.md | 329 +++++ .../develop/ui/compose/tooling/debug.md | 114 ++ .../recipes/material-listdetail.md | 141 ++ navigation/navigation-3/SKILL.md | 2 +- .../navigation-3/recipes/results-event.md | 151 +- .../navigation-3/recipes/results-state.md | 141 +- play/engage-sdk-integration/SKILL.md | 13 +- .../android/guide/playcore/engage/read.md | 8 +- .../references/schemas/food.md | 509 +++++++ .../references/schemas/listen.md | 842 +++++++++++ .../references/schemas/other.md | 220 +++ .../references/schemas/read.md | 412 ++++++ .../references/schemas/shopping.md | 357 +++++ .../references/schemas/social.md | 228 +++ .../references/schemas/travel.md | 923 +++++++++++++ .../references/schemas/tv.md | 1226 +++++++++++++++++ .../references/schemas/watch.md | 1137 +++++++++++++++ testing/testing-setup/SKILL.md | 198 +++ .../ui/compose/testing/common-patterns.md | 181 +++ .../preview/compose-screenshot-testing.md | 227 +++ .../dependency-injection/hilt-testing.md | 447 ++++++ 30 files changed, 8800 insertions(+), 269 deletions(-) create mode 100644 jetpack-compose/adaptive/SKILL.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md create mode 100644 jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md create mode 100644 jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md create mode 100644 play/engage-sdk-integration/references/schemas/food.md create mode 100644 play/engage-sdk-integration/references/schemas/listen.md create mode 100644 play/engage-sdk-integration/references/schemas/other.md create mode 100644 play/engage-sdk-integration/references/schemas/read.md create mode 100644 play/engage-sdk-integration/references/schemas/shopping.md create mode 100644 play/engage-sdk-integration/references/schemas/social.md create mode 100644 play/engage-sdk-integration/references/schemas/travel.md create mode 100644 play/engage-sdk-integration/references/schemas/tv.md create mode 100644 play/engage-sdk-integration/references/schemas/watch.md create mode 100644 testing/testing-setup/SKILL.md create mode 100644 testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md create mode 100644 testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md create mode 100644 testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md new file mode 100644 index 0000000..93cf80f --- /dev/null +++ b/jetpack-compose/adaptive/SKILL.md @@ -0,0 +1,301 @@ +--- +name: adaptive +description: Instructions to make or update an app's UI so that it adapts to different + Android devices including phones, tablets, foldables, laptops, desktop, TV, Auto + and XR. It includes how to handle different window sizes, pointing devices (such + as mouse) and text entry devices (such as keyboard) using the Compose MediaQuery + API. It also covers multi-pane layouts using Navigation3 Scenes, adaptive UI components + (such as buttons) with varying target sizes, and adaptive layouts (including navigation + areas - nav rails and nav bars) using the Compose Grid and FlexBox APIs. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-15' + keywords: + - android + - ui + - adaptive + - Grid + - FlexBox + - MediaQuery + - navigation +--- + +## Prerequisites + +The app must: + +- Use Compose for all screens. If it's still using Fragments or Views, suggest using the XML to Compose skill to migrate those screens. +- Use Jetpack Navigation 3. If it doesn't, suggest the Navigation 3 skill to migrate the app. + +## Workflow to make an app adaptive + +To make an app adaptive, follow these steps or a subset of them adapting to the +task. + +- Step 1: Verify current UI +- Step 2: Make the navigation bar adaptive +- Step 3: Add multi-pane layouts +- Step 4: Make vertical lists adaptive by changing the number of columns +- Step 5: Hide app bars when scrolling + +## Step 1. Verify current UI + +Ensure that screenshot tests exist to verify the current UI on different form +factors. If they don't exist, add the [Compose Preview Screenshot Testing +tool](references/android/develop/ui/compose/tooling/debug.md). Use the following annotation to create previews for all the major form +factors. For example: + + +```kotlin +@Preview(name = "Phone", device = Devices.PHONE, showBackground = true) +@Preview(name = "Foldable", device = Devices.FOLDABLE, showBackground = true) +@Preview(name = "Tablet", device = Devices.TABLET, showBackground = true) +@Preview(name = "Desktop", device = Devices.DESKTOP, showBackground = true) +annotation class FormFactorPreviews + +@PreviewTest +@FormFactorPreviews +@Composable +fun FeedScreenPreview() { + SnippetsTheme { + Box { + Text("My Screen") + } + } +} +``` + +
+ +## 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). + +If you need to provide more screen real state 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 the content is best displayed in full screen (such as a single photo screen). + +When the detail screen is displayed full-screen on mobile, full-screen mode must +be deactivated on larger screens. + +Steps to migrate: + +- Locate the existing navigation bar. +- Convert each item to a `NavigationSuiteItem`. +- Identify whether the navigation bar's visibility changes. For example, if it is wrapped with an `AnimatedContent` or `AnimatedVisibility` composable. If so, follow the guidance in the "Control navigation area visibility". +- Replace the container that held the navigation bar (often a `Scaffold`) with `NavigationSuiteScaffold` from the Material 3 adaptive layouts library. +- Supply the navigation items using the `navigationItems` parameter of `NavigationSuiteScaffold`. + +### 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. + +Steps to migrate: + +- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. It could be named something like `isNavBarVisible` or `shouldShowNavBar`. +- Create an instance of `NavigationSuiteScaffoldState` using `rememberNavigationSuiteScaffoldState()` and pass it to `NavigationSuiteScaffold`. +- When the navigation area visibility changes, use a `LaunchedEffect` to call `show` or `hide` on the `NavigationSuiteScaffoldState`. + +For example: + + +```kotlin +// Pass this variable to any composable that needs to control the navigation area visibility +var isNavBarVisible by remember { mutableStateOf(true) } +val scaffoldVisibilityState = rememberNavigationSuiteScaffoldState() + +NavigationSuiteScaffold( + navigationSuiteItems = navItems, + state = scaffoldVisibilityState +) { + // Main content +} + +LaunchedEffect(isNavBarVisible){ + if (isNavBarVisible) { + scaffoldVisibilityState.show() + } else { + 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. + +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). + +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). + +#### Add a Material list-detail SceneStrategy + +- Add the `androidx.compose.material3.adaptive:adaptive-navigation3` library +- Create an `androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy` using `rememberListDetailSceneStrategy` +- Pass the `ListDetailSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter + +#### 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 = { + })`. +- 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()`. + +#### Important considerations + +- 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). + +### 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. + +#### Add a Material supporting pane `SceneStrategy` + +- If you haven't already, add the `androidx.compose.material3.adaptive:adaptive-navigation3` library +- Create an `androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy` using `rememberSupportingPaneSceneStrategy` +- Pass the `SupportingPaneSceneStrategy` to `NavDisplay` using its `sceneStrategies` parameter + +#### Use metadata to identify the main and supporting screens + +- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the main entry using `SupportingPaneSceneStrategy.mainPane()` +- Add metadata to the supporting entry using `SupportingPaneSceneStrategy.supportingPane()` + +### 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. + +## 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`. + +Steps to migrate: + +- Choose a suitable minimum width in dp for the column. It should be large enough so that item is clearly visible to the user. +- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction below +- For `LazyVerticalGrid`: change the `columns` parameter to use `GridCells.Adaptive(.dp)` +- For `LazyVerticalStaggeredGrid`: change the `columns` parameter to use `StaggeredGridCells.Adaptive(.dp)` + +### 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. + +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: + +- less than 800dp, a 2x4 grid is used +- 800dp or more, a 4x2 grid is used + + +```kotlin +Grid( + config = { + val maxWidthDp = constraints.maxWidth.toDp() + val (cols, rows) = if (maxWidthDp < 800.dp){ + 2 to 4 + } else{ + 4 to 2 + } + + val gapSizeDp = 8.dp + val cellSize = ((maxWidthDp - (gapSizeDp * (cols - 1))) / cols).coerceAtLeast(0.dp) + repeat(cols) { column(cellSize) } + repeat(rows) { row(cellSize) } + gap(gapSizeDp) + } +) { /** items **/ } +``` + +
+ +`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: + +- `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. + +## Additional documentation for experimental adaptive APIs + +The following APIs are available from Compose 1.11.0-beta01. + +### FlexBox + +Check the FlexBox documentation: + +- [Overview](references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md) +- [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md) +- [Set container behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md) +- [Set item behavior](references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md) + +## 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. + +## Grid + +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) +- [Set container properties](references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md) +- [Set item properties](references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.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 new file mode 100644 index 0000000..ad4862f --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md @@ -0,0 +1,112 @@ +To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` +block and supply it using the `config` parameter. + + +```kotlin +FlexBox( + config = { + direction(FlexDirection.Column) + wrap(FlexWrap.Wrap) + alignItems(FlexAlignItems.Center) + alignContent(FlexAlignContent.SpaceAround) + justifyContent(FlexJustifyContent.Center) + gap(16.dp) + } +) { // child 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: + +- `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`. +- `Column`: Sets the main axis to be vertical, top-to-bottom. +- `ColumnReverse`: Reverses the direction of `Column`. + +## Align items and distribute extra space + +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`. + +|---|---| +| | ![Illustration of a horizontal main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/main-axis.png) | +| `Start` | ![Items aligned to the start of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-start.png) | +| `Center` | ![Items aligned to the center of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-center.png) | +| `End` | ![Items aligned to the end of the main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-end.png) | +| `SpaceBetween` | ![Items distributed along the main axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacebetween.png) | +| `SpaceAround` | ![Items distributed along the main axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spacearound.png) | +| `SpaceEvenly` | ![Items distributed along the main axis with space evenly around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/mainaxis-spaceevenly.png) | + +### 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). + +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`: + +|---|---|---|---|---|---|---| +| ![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) | +| | `Start` | `End` | `Center` | `Stretch` | `SpaceBetween` | `SpaceAround` | + +## 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`. + +|---|---| +| **`FlexWrap` value** | **Example using direction `Row`** | +| `NoWrap` (default): Prevents items from wrapping. Items overflow if the main size is insufficient. | ![Items in a single line overflowing the container because wrapping is disabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-1.png) | +| `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. + +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. +![First item placed in the FlexBox container.](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. +![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. + +|---|---|---| +| ![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) | +| `rowGap` adds vertical space between items and lines. | `columnGap` adds horizontal space between items and lines. | `gap` is a convenience function that adds both `columnGap` and `rowGap`. | \ No newline at end of file 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 new file mode 100644 index 0000000..9164652 --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md @@ -0,0 +1,69 @@ +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`. + + [versions] + compose = "1.12.0-alpha02" + + [libraries] + androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } + +2. Add the library dependency to your app's `build.gradle.kts`. + + dependencies { + implementation(libs.androidx.compose.foundation.layout) + } + +## Create basic FlexBox layouts + +**Example 1** : `FlexBox` lays out two `Text` elements that are centrally +aligned. + + +```kotlin +FlexBox( + config = { + direction(FlexDirection.Column) + alignItems(FlexAlignItems.Center) + } +) { + 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. + + +```kotlin +FlexBox( + config = { + wrap(FlexWrap.Wrap) + gap(8.dp) + } +) { + // All boxes have an intrinsic width of 100.dp + // Some grow to fill any remaining space on the row. + RedRoundedBox() + BlueRoundedBox() + GreenRoundedBox(modifier = Modifier.flex { grow(1.0f) }) + 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 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 new file mode 100644 index 0000000..ddda0de --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md @@ -0,0 +1,81 @@ +> [!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. + +With `FlexBox`, you can: + +- Control how items grow and shrink to fill the available space +- Wrap items onto new rows or columns when there isn't enough space for them +- Distribute extra space between items using convenient presets + +## 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`. + +## 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 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`. + +
+ +### Apply properties + +You can apply `FlexBox` properties in two ways: + +- To the `FlexBox` container using `FlexBox(config)` +- To an item inside the `FlexBox` using `Modifier.flex` + +| **Container properties (`config`**) | **Item properties (`Modifier.flex`**) | +|---|---| +| - `direction` - the item layout direction - `wrap` - whether to wrap items if the **main size** is insufficient - `justifyContent` - how to **distribute** items along the **main axis** - `alignItems` - how to **align** items along the **cross axis** - `alignContent` - how to distribute extra space from the **cross size** when there are multiple lines - `rowGap` / `columnGap` - adds space between items and lines See [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) for more information about these properties. | - `basis` - the size of the item before any extra space from the **main size** is distributed - `grow` - the share of extra space from the **main size** that this item should receive - `shrink` - the share of space deficit from the **main size** that this item should receive - `alignSelf` - how to distribute extra space from the **cross size** to this item, overrides `alignItems` - `order` - controls the layout order See [Set item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior) for more information about these properties. | + +### 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. + +`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. + +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). + +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). + +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 new file mode 100644 index 0000000..e4a5c0f --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md @@ -0,0 +1,170 @@ +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 { + RedRoundedBox( + modifier = Modifier.flex { + basis(FlexBasis.Auto) + grow(1.0f) + shrink(0.5f) + } + ) +} +``` + +
+ +### 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. + +|---|---|---|---| +| **Value type** | **Behavior** | **Code snippet** Note: The boxes have a maximum intrinsic size of `100dp` | **Example using container width `600dp`** | +| `Auto` (default) | Use the item's maximum intrinsic size. For example, a `Text` composable's maximum intrinsic width is the width of all its text on a single line - no wrapping. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) BlueRoundedBox( Modifier.flex { basis(FlexBasis.Auto) } ) } ``` | ![Items sized based on their intrinsic size using basis Auto.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-1.png) | +| 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. + +### 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. + +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. + +|---|---| +| ```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. + +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 { + Text( + "The quick brown fox", + fontSize = 36.sp, + modifier = Modifier + .background(PastelRed) + .flex { shrink(1f) } + ) + Text( + "The quick brown fox", + fontSize = 36.sp, + modifier = Modifier + .background(PastelBlue) + .flex { shrink(0f) } + ) +} +``` + +
+ +As the container size shrinks, Child 1 shrinks. + +|---|---| +| **Container size** | **FlexBox UI** | +| `700dp` | ![Two items in a 700dp container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-1.png) | +| `500dp` | ![First item shrinks as container size reduces to 500dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-2.png) | +| `450dp` | ![First item shrinks further as container size reduces to 450dp.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/containersize-3.png) | + +## 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. + +For example, this `FlexBox` has `alignItems` set to `Start` and five children +which override the cross axis alignment. + + +```kotlin +FlexBox( + config = { + alignItems(FlexAlignItems.Start) + } +) { + RedRoundedBox() + BlueRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Center) }) + GreenRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.End) }) + PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) }) + OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) }) +} +``` + +
+ +![Five children of varying sizes overriding the alignItems property.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/item-alignment.png) + +## Item 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 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 { + // Declared first, but will be placed after visually + RedRoundedBox( + title = "World" + ) + + // Declared second, but will be placed first visually + BlueRoundedBox( + title = "Hello", + modifier = Modifier.flex { + order(-1) + } + ) +} +``` + +
+ +![Two rounded boxes, with the first containing the text Hello and the second containing the text World.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/itemorder.png) \ No newline at end of file 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 new file mode 100644 index 0000000..63d160c --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md @@ -0,0 +1,238 @@ +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. +- [Manage track sizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-track-size): Use fixed, percentage, flexible, and intrinsic sizing to set track sizes. +- [Set gaps](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-gap): Manage the "gutters" between rows and columns. + +## 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): + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + } +) { +} +``` + +
+ +## 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. + +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( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + } +) { + Card1() + Card2() + Card3() + Card4() + 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()). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) + flow = GridFlow.Column // Grid tries to place items to fill the column + }, +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
+ +![The flow function changes the direction to place items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-flow.png) **Figure 2** . `GridFlow.Row` (left) and `GridFlow.Column` (right). + +## 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: + +- **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#intrisic-grid-track-size). + +The following example uses the different track sizing options +to define the row heights: + + +```kotlin +Grid( + config = { + column(1f) + + row(100.dp) + row(0.2f) + row(1.fr) + row(GridTrackSize.Auto) + }, + modifier = Modifier.height(480.dp) +) { + PastelRedCard("Fixed(100.dp)") +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-sizes.png) **Figure 3** . Row heights defined using the four primary track sizing options in `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: + +- [`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. + + +```kotlin +Grid( + config = { + column(GridTrackSize.MinContent) + column(GridTrackSize.MaxContent) + row(1.0f) + }, + modifier = Modifier.width(480.dp) +) { + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." ) + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." ) +} +``` + +
+ +![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 4**. Intrinsic sizes specified in the columns. + +## 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). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(16.dp) + columnGap(8.dp) + } +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
+ +![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 5**. 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: + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) // Equivalent to columnGap(8.dp) and rowGap(8.dp) + } +) { + Card1() + Card2() + Card3() + Card4() + Card5() + Card6() +} +``` + +
\ 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 new file mode 100644 index 0000000..1c51d22 --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md @@ -0,0 +1,51 @@ +This page describes how to implement basic [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) 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`. + + [versions] + compose = "1.12.0-alpha02" + + [libraries] + androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } + +2. Add the library dependency to your app's `build.gradle.kts`. + + dependencies { + implementation(libs.androidx.compose.foundation.layout) + } + +## Create a basic grid + +The following example creates a basic 2x3 grid, +with the columns and rows having a fixed size of `100.dp`. + + +```kotlin +Grid( + config = { + repeat(2) { + column(100.dp) + } + repeat(3) { + row(100.dp) + } + } +) { + Card1(containerColor = PastelRed) + Card2(containerColor = PastelGreen) + Card3(containerColor = PastelBlue) + Card4(containerColor = PastelPink) + 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 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 new file mode 100644 index 0000000..1f74aee --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md @@ -0,0 +1,73 @@ +> [!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. +![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. + +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: + +| Component | Purpose | +|---|---| +| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that requires lazy loading. | +| `Row`, `Column`, `FlexBox` | One-dimensional layout | +| `Grid` | Two-dimensional layout | + +> [!NOTE] +> **Note:** `Grid` doesn't support lazy loading. + +## Terminology + +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: +![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 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 + +A grid cell is the intersection of a row and column track. +![A grid cell that is an intersection of the second row and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-cell.png) **Figure 4**. A grid cell that is an intersection of the second row and the second column. + +### 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 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 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 new file mode 100644 index 0000000..f5ecc87 --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md @@ -0,0 +1,168 @@ +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. + +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 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). + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + gap(8.dp) + } +) { + Card1() + Card2(modifier = Modifier.gridItem(row = 2, column = 2)) + Card3(modifier = Modifier.gridItem(row = -1, column = -2)) +} +``` + +
+ +![Card #2 is placed in the grid cell +in the second row and the second column, +and Card #3 is placed in the first column in the third row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/position.png) **Figure 1** . Card **#2** is placed in the grid cell in the second row and the second column, and Card **#3** is placed in the first column in the third row. + +## 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). + + +```kotlin +Grid( + config = { + repeat(3) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(8.dp) + columnGap(8.dp) + } +) { + Card1(modifier = Modifier.gridItem(rowSpan = 2, columnSpan = 2)) + Card2() + Card3() + Card4(modifier = Modifier.gridItem(columnSpan = 3)) +} +``` + +
+ +![Card #4 spans three columns](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/spanning.png) **Figure 2** . Card **#4** spans three columns. + +## 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. + + +```kotlin +Grid( + config = { + repeat(3) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(8.dp) + columnGap(8.dp) + }, +) { + Text( + text = "#1", + modifier = Modifier + .gridItem( + rowSpan = 2, + columnSpan = 2, + alignment = Alignment.Center + ), + ) + Card2() + Card3() + Card4(modifier = Modifier.gridItem(columnSpan = 3)) +} +``` + +
+ +![The Text with #1 is placed in the center of the grid area +consisting of two rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/alignment.png) **Figure 3** . The Text with **#1** is placed in the center of the grid area consisting of two rows and two columns. + +## 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 specified grid cells, +and the other items are auto-placed. + + +```kotlin +Grid( + config = { + repeat(2) { + column(160.dp) + } + repeat(3) { + row(90.dp) + } + rowGap(16.dp) + columnGap(8.dp) + } +) { + Card1() + Card2(modifier = Modifier.gridItem(row = 2, column = 2)) + Card3() + Card4(modifier = Modifier.gridItem(row = 3, column = 1)) + Card5() + Card6() +} +``` + +
+ +![Card #3 is placed next to card #1, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to card **#1**, as it is an auto-placement. \ No newline at end of file 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 new file mode 100644 index 0000000..4cd2564 --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md @@ -0,0 +1,329 @@ +> [!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: + +- Window posture +- Pointing devices precision +- Keyboard type +- 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: + + +```kotlin +@Composable +fun VideoPlayer( + // ... +) { + // ... + if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { + TabletopLayout() + } else { + FlatLayout() + } + // ... +} +``` + +
+ +## 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`: + + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + ComposeUiFlags.isMediaQueryIntegrationEnabled = true + 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`. + +| Parameter | Value type | Description | +|---|---|---| +| `windowWidth` | [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp) | The current window width in dp. | +| `windowHeight` | `Dp` | The current window height in dp. | +| `windowPosture` | [`UiMediaScope.Posture`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture) | The current posture of the application window. | +| `pointerPrecision` | [`UiMediaScope.PointerPrecision`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision) | The highest precision of the available pointing devices. | +| `keyboardKind` | [`UiMediaScope.KeyboardKind`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind) | The type of keyboard available or connected. | +| `hasCamera` | `Boolean` | Whether the camera is supported on the device. | +| `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. + + +```kotlin +@Composable +fun VideoPlayer( + // ... +) { + // ... + if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) { + TabletopLayout() + } else { + FlatLayout() + } + // ... +} +``` + +
+ +### 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). + +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 { + windowWidth < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND.dp +} +val narrowerThanExpanded by derivedMediaQuery { + windowWidth < WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND.dp +} +when { + narrowerThanMedium -> SinglePaneLayout() + narrowerThanExpanded -> TwoPaneLayout() + else -> ThreePaneLayout() +} +``` + +
+ +**Figure 1**. Layout is updated according to the window width. + +### 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: + + +```kotlin +when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + 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. + +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. + +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 }) { + LargeSizeButton() +} 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: + + +```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: + + +```kotlin +Row { + OutlinedTextField(state = rememberTextFieldState()) + // Show the MicButton when the device supports a microphone. + if (mediaQuery { hasMicrophone }) { + MicButton() + } + // Show the CameraButton when the device supports a camera. + if (mediaQuery { hasCamera }) { + 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. + +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 { + mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Far } -> 20.sp + 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()). + + +```kotlin +when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + 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: + +1. Enable the `mediaQuery` function. +2. Define a custom object that implements the `UiMediaScope` interface. +3. Set the custom object to the `LocalUiMediaScope` with the [`CompositionLocalProvider`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocalProvider.composable#CompositionLocalProvider(androidx.compose.runtime.CompositionLocalContext,kotlin.Function0)) function. +4. Call the composable to preview in the content lambda of the `CompositionLocalProvider` function. + +You can preview the `TabletopLayout` with the following example: + + +```kotlin +@Preview +@Composable +fun PreviewLayoutForTabletop() { + // Step 1: Enable the mediaQuery function + ComposeUiFlags.isMediaQueryIntegrationEnabled = true + + val currentUiMediaScope = LocalUiMediaScope.current + // Step 2: Define a custom object implementing the UiMediaScope interface. + // The object overrides the windowPosture parameter. + // The resolution of the remaining parameters is deferred to the currentUiMediaScope object. + val uiMediaScope = remember(currentUiMediaScope) { + object : UiMediaScope by currentUiMediaScope { + override val windowPosture: UiMediaScope.Posture = UiMediaScope.Posture.Tabletop + } + } + + // Step 3: Set the object to the LocalUiMediaScope. + CompositionLocalProvider(LocalUiMediaScope provides uiMediaScope) { + // Step 4: Call the composable to preview. + when { + mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() + mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() + } + } +} +``` + +
\ 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 new file mode 100644 index 0000000..77bc9da --- /dev/null +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md @@ -0,0 +1,114 @@ +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. + +> [!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. + +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. + +> [!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**. + +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 +> icon](https://developer.android.com/static/studio/images/buttons/live-layout-inspector-view-options-icon.png) near the top of the **Component Tree** pane, as shown in the following image. + +![Enable the composition and skip counter in Layout +Inspector](https://developer.android.com/static/develop/ui/compose/images/li-show-recomposition-counts.png) + +**Figure 3**. Enable the composition and skip counter in Layout Inspector. + +### Compose semantics + +In Compose, [Semantics](https://developer.android.com/develop/ui/compose/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. + +## 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. + +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. +![](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. +![](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 new file mode 100644 index 0000000..fbaae79 --- /dev/null +++ b/jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md @@ -0,0 +1,141 @@ +# Material List-Detail Recipe + +This recipe demonstrates how to create an adaptive list-detail layout using the `ListDetailSceneStrategy` from the Material 3 Adaptive library. This layout automatically adjusts to show one, two, or three panes depending on the available screen width. + +## How it works + +This example has three destinations: `ConversationList`, `ConversationDetail`, and `Profile`. + +### `ListDetailSceneStrategy` + +The key to this recipe is the `rememberListDetailSceneStrategy`, which provides the logic for the adaptive layout. + +- **Pane Roles**: Each destination is assigned a role using metadata: + + - `ListDetailSceneStrategy.listPane()`: For the primary (list) content. This pane is always visible. A placeholder can be provided to be shown in the detail pane area when no detail content is selected. + - `ListDetailSceneStrategy.detailPane()`: For the secondary (detail) content. + - `ListDetailSceneStrategy.extraPane()`: For tertiary content. +- **Adaptive Layout** : The `ListDetailSceneStrategy` automatically handles the layout. On smaller screens, only one pane is shown at a time. On wider screens, it will show the list and detail panes side-by-side. On very wide screens, it can show all three panes: list, detail, and extra. + +- **Navigation** : Navigation between the panes is handled by adding and removing destinations from the back stack as usual. The `ListDetailSceneStrategy` observes the back stack and adjusts the layout accordingly. + +[![](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/material/listdetail) + +``` +package com.example.nav3recipes.material.listdetail + +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.content.ContentYellow +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private object ConversationList : NavKey + +@Serializable +private data class ConversationDetail(val id: String) : NavKey + +@Serializable +private data object Profile : NavKey + +class MaterialListDetailActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + + val backStack = rememberNavBackStack(ConversationList) + + // Override the defaults so that there isn't a horizontal space between the panes. + // See b/418201867 + val windowAdaptiveInfo = currentWindowAdaptiveInfoV2() + val directive = remember(windowAdaptiveInfo) { + calculatePaneScaffoldDirective(windowAdaptiveInfo) + .copy(horizontalPartitionSpacerSize = 0.dp) + } + val listDetailStrategy = rememberListDetailSceneStrategy(directive = directive) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sceneStrategies = listOf(listDetailStrategy), + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane( + detailPlaceholder = { + ContentYellow("Choose a conversation from the list") + } + ) + ) { + ContentRed("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(ConversationDetail("ABC")) + }) { + Text("View conversation") + } + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane() + ) { conversation -> + ContentBlue("Conversation ${conversation.id} ") { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = dropUnlessResumed { + backStack.add(Profile) + }) { + Text("View profile") + } + } + } + } + entry( + metadata = ListDetailSceneStrategy.extraPane() + ) { + ContentGreen("Profile") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index 7c9afbb..ef1f7ff 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-05-05' + last-updated: '2026-05-15' keywords: - recipe - Android 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 c8461fc..a61736f 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 @@ -6,9 +6,10 @@ This recipe demonstrates how to return a result from one screen to a previous sc This example uses a `ResultEventBus` to facilitate communication between the screens. -1. **`ResultEventBus`**: A simple event bus is created and made available to the composables. -2. **Sending the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back as a one-time event. -3. **Receiving the result** : The screen that needs the result uses a `ResultEffect` composable to listen for results of a specific type. When a result is received, the effect's lambda is triggered. +1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. +2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. +3. **Sending the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back as a one-time event. +4. **Receiving the result** : The screen that needs the result uses a `ResultEffect` composable to listen for results of a specific type. When a result is received, the effect's lambda is triggered. This approach is useful for results that are transient and should be handled as one-time events. [![](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/results/event) @@ -201,52 +202,6 @@ fun PersonDetailsScreen( package com.example.nav3recipes.results.event -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect - -/** - * An Effect to provide a result even between different screens - * - * The trailing lambda provides the result from a flow of results. - * - * @param resultEventBus the ResultEventBus to retrieve the result from. The default value - * is read from the `LocalResultEventBus` composition local. - * @param resultKey the key that should be associated with this effect - * @param onResult the callback to invoke when a result is received - */ -@Composable -inline fun ResultEffect( - resultEventBus: ResultEventBus = LocalResultEventBus.current, - resultKey: String = T::class.toString(), - crossinline onResult: suspend (T) -> Unit -) { - LaunchedEffect(resultKey, resultEventBus.channelMap[resultKey]) { - resultEventBus.getResultFlow(resultKey)?.collect { result -> - onResult.invoke(result as T) - } - } -} -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.event - import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -257,6 +212,10 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.result.LocalResultEventBus +import androidx.navigation3.runtime.result.ResultEffect +import androidx.navigation3.runtime.result.ResultEventBus +import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.results.common.Home import com.example.nav3recipes.results.common.HomeScreen @@ -273,8 +232,6 @@ class ResultEventActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - val resultBus = remember { ResultEventBus() } - Scaffold { paddingValues -> val backStack = rememberNavBackStack(Home) @@ -283,10 +240,11 @@ class ResultEventActivity : ComponentActivity() { backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, + entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), entryProvider = entryProvider { entry { val viewModel = viewModel(key = Home.toString()) - ResultEffect(resultBus) { person -> + ResultEffect { person -> viewModel.person = person } @@ -297,6 +255,7 @@ class ResultEventActivity : ComponentActivity() { ) } entry { + val resultBus = LocalResultEventBus.current PersonDetailsScreen( onSubmit = { person -> resultBus.sendResult(result = person) @@ -310,92 +269,4 @@ class ResultEventActivity : ComponentActivity() { } } } -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.event - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.ProvidedValue -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.mutableStateMapOf -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.BUFFERED -import kotlinx.coroutines.flow.receiveAsFlow - -/** - * Local for receiving results in a [ResultEventBus] - */ -object LocalResultEventBus { - private val LocalResultEventBus: ProvidableCompositionLocal = - compositionLocalOf { null } - - /** - * The current [ResultEventBus] - */ - val current: ResultEventBus - @Composable - get() = LocalResultEventBus.current ?: error("No ResultEventBus has been provided") - - /** - * Provides a [ResultEventBus] to the composition - */ - infix fun provides( - bus: ResultEventBus - ): ProvidedValue { - return LocalResultEventBus.provides(bus) - } -} -/** - * An EventBus for passing results between multiple sets of screens. - * - * It provides a solution for event based results. - */ -class ResultEventBus { - /** - * Map from the result key to a channel of results. - */ - val channelMap = mutableStateMapOf>() - - /** - * Provides a flow for the given resultKey. - */ - inline fun getResultFlow(resultKey: String = T::class.toString()) = - channelMap[resultKey]?.receiveAsFlow() - - /** - * Sends a result into the channel associated with the given resultKey. - */ - inline fun sendResult(resultKey: String = T::class.toString(), result: T) { - if (!channelMap.contains(resultKey)) { - channelMap[resultKey] = Channel(capacity = BUFFERED, onBufferOverflow = BufferOverflow.SUSPEND) - } - channelMap[resultKey]?.trySend(result) - } - - /** - * Removes all results associated with the given key from the store. - */ - inline fun removeResult(resultKey: String = T::class.toString()) { - channelMap.remove(resultKey) - } -} ``` \ 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 6a61883..71c2ccf 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 @@ -4,13 +4,14 @@ This recipe demonstrates how to return a result from one screen to a previous sc ## How it works -This example uses a `ResultStore` to manage the result as state. +This example uses a `ResultEventBus` to manage the result as state. -1. **`ResultStore`** : A `ResultStore` is created and made available to the composables. This store holds the results. -2. **Setting the result** : The screen that produces the result calls `resultStore.setResult(person)` to save the data in the store. -3. **Observing the result** : The screen that needs the result calls `resultStore.getResultState()` to get a `State` object representing the result. The UI then observes this state and recomposes whenever the result changes. +1. **ResultEventBusNavEntryDecorator** : A `NavEntryDecorator` that provides a `ResultEventBus` via `LocalResultEventBus`. +2. **`ResultEventBus`** : A `ResultEventBus` is created and made available to the composables via `LocalResultEventBus`. This EventBus sends and receives the results. +3. **Setting the result** : The screen that produces the result calls `resultBus.sendResult(person)` to send the data back. +4. **Observing the result** : The screen that needs the result calls `resultBus.conflateAsState()` to get a `State` object representing the result. The UI then observes this state and recomposes whenever the result changes. -This approach is suitable when the result should be treated as persistent state that survives recomposition and configuration changes. +This approach is suitable when only the latest result is required. The result state does not survive configuration change or process death. [![](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/results/state) ``` @@ -207,11 +208,16 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.result.LocalResultEventBus +import androidx.navigation3.runtime.result.ResultEffect +import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.results.common.Home import com.example.nav3recipes.results.common.HomeScreen +import com.example.nav3recipes.results.common.HomeViewModel import com.example.nav3recipes.results.common.Person import com.example.nav3recipes.results.common.PersonDetailsForm import com.example.nav3recipes.results.common.PersonDetailsScreen @@ -224,27 +230,29 @@ class ResultStateActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - val resultStore = rememberResultStore() - Scaffold { paddingValues -> val backStack = rememberNavBackStack(Home) - NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, + entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), entryProvider = entryProvider { entry { - val person = resultStore.getResultState() + val resultState = LocalResultEventBus + .current + .conflateAsState(null) + val person = resultState.value HomeScreen( person = person, onNext = { backStack.add(PersonDetailsForm()) } ) } entry { + val resultBus = LocalResultEventBus.current PersonDetailsScreen( onSubmit = { person -> - resultStore.setResult(result = person) + resultBus.sendResult(result = person) backStack.removeLastOrNull() } ) @@ -255,117 +263,4 @@ class ResultStateActivity : ComponentActivity() { } } } -``` - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.results.state - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.ProvidedValue -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.mapSaver -import androidx.compose.runtime.saveable.rememberSaveable - -/** - * Local for storing results in a [ResultStore] - */ -object LocalResultStore { - private val LocalResultStore: ProvidableCompositionLocal = - compositionLocalOf { null } - - /** - * The current [ResultStore] - */ - val current: ResultStore - @Composable - get() = LocalResultStore.current ?: error("No ResultStore has been provided") - - /** - * Provides a [ResultStore] to the composition - */ - infix fun provides( - store: ResultStore - ): ProvidedValue { - return LocalResultStore.provides(store) - } -} - -/** - * Provides a [ResultStore] that will be remembered across configuration changes. - */ -@Composable -fun rememberResultStore(): ResultStore { - return rememberSaveable(saver = ResultStoreSaver()) { - ResultStore() - } -} - -/** - * A store for passing results between multiple sets of screens. - * - * It provides a solution for state based results. - */ -class ResultStore { - - /** - * Map from the result key to a mutable state of the result. - */ - val resultStateMap = mutableStateMapOf>() - - /** - * Retrieves the current result of the given resultKey. - */ - inline fun getResultState(resultKey: String = T::class.toString()) = - resultStateMap[resultKey]?.value as T - - /** - * Sets the result for the given resultKey. - */ - inline fun setResult(resultKey: String = T::class.toString(), result: T) { - resultStateMap[resultKey] = mutableStateOf(result) - } - - /** - * Removes all results associated with the given key from the store. - */ - inline fun removeResult(resultKey: String = T::class.toString()) { - resultStateMap.remove(resultKey) - } -} - -/** Saver to save and restore the ResultStore across config change and process death. */ -private fun ResultStoreSaver(): Saver = - mapSaver( - save = { resultStore -> - resultStore.resultStateMap.mapValues { it.value.value } - }, - restore = { restoredMap -> - ResultStore().apply { - restoredMap.forEach { (key, value) -> - resultStateMap[key] = mutableStateOf(value) - } - } - } - ) ``` \ No newline at end of file diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 8c2b77e..7c4548d 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-05-14' + last-updated: '2026-05-15' keywords: - android - engage @@ -116,3 +116,14 @@ Follow these steps to assist the developer: - [TV Recommendations](references/android/guide/playcore/engage/tv/recommendations.md) - [TV Continue Watching](references/android/guide/playcore/engage/tv/continue-watching/index.md) - [TV Entitlements](references/android/guide/playcore/engage/tv/entitlements.md) +- **Vertical-Specific Schemas:** + + - [Food Schema](references/schemas/food.md) + - [Watch Schema](references/schemas/watch.md) + - [Listen Schema](references/schemas/listen.md) + - [Read Schema](references/schemas/read.md) + - [Shopping Schema](references/schemas/shopping.md) + - [Social Schema](references/schemas/social.md) + - [Travel Schema](references/schemas/travel.md) + - [TV Schema](references/schemas/tv.md) + - [Other Schema](references/schemas/other.md) 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 4edff8a..d1abfb8 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 @@ -159,11 +159,11 @@ Potter* book series, which has 7 books). Required specifications for image assets are listed below: -| Aspect ratio | Requirement | Minimum pixels | Recommended pixels | +| Aspect ratio | Supported cluster(s) | Minimum pixels | Recommended pixels | |---|---|---|---| -| Square (1x1) | **Required** | 300x300 | 1200x1200 | -| Landscape (1.91x1) | Optional | 600x314 | 1200x628 | -| Portrait (4x5) | Optional | 480x600 | 960x1200 | +| Square (1x1) | All clusters | 300x300 | 1200x1200 | +| Landscape (1.91x1) | Featured and continuation | 600x314 | 1200x628 | +| Portrait (4x5) | Recommendation | 480x600 | 960x1200 | *File formats* diff --git a/play/engage-sdk-integration/references/schemas/food.md b/play/engage-sdk-integration/references/schemas/food.md new file mode 100644 index 0000000..8c8e6fa --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/food.md @@ -0,0 +1,509 @@ +This file defines the schema for the FOOD vertical in the Engage SDK. + + { + "client": "com.google.android.engage.food.service.AppEngageFoodClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_FOOD_SHOPPING_CART", + "TYPE_FOOD_SHOPPING_LIST", + "TYPE_FOOD_REORDER" + ], + "entities": { + "ProductEntity": { + "package": "com.google.android.engage.food.datamodel.ProductEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Required if strikethrough price is provided", + "Mutually required with other rating fields" + ] + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "callout": { + "type": "String", + "requirement": "Optional", + "setter": "setCallout(String)", + "getter": "getCallout()" + }, + "calloutFinePrint": { + "type": "String", + "requirement": "Optional", + "setter": "setCalloutFinePrint(String)", + "getter": "getCalloutFinePrint()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + } + } + }, + "StoreEntity": { + "package": "com.google.android.engage.food.datamodel.StoreEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Mutually required with other rating fields" + ] + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "location": { + "type": "String", + "requirement": "Optional", + "setter": "setLocation(String)", + "getter": "getLocation()" + }, + "category": { + "type": "String", + "requirement": "Optional", + "setter": "setCategory(String)", + "getter": "getCategory()" + }, + "callout": { + "type": "String", + "requirement": "Optional", + "setter": "setCallout(String)", + "getter": "getCallout()" + }, + "calloutFinePrint": { + "type": "String", + "requirement": "Optional", + "setter": "setCalloutFinePrint(String)", + "getter": "getCalloutFinePrint()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "RestaurantReservationEntity": { + "package": "com.google.android.engage.food.datamodel.RestaurantReservationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "location": { + "type": "Address", + "requirement": "Required", + "setter": "setLocation(Address)", + "getter": "getLocation()" + }, + "reservationStartTime": { + "type": "Long", + "requirement": "Required", + "setter": "setReservationStartTime(Long)", + "getter": "getReservationStartTime()" + }, + "localizedReservationStartTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedReservationStartTime(LocalizedTimestamp)", + "getter": "getLocalizedReservationStartTime()" + }, + "tableSize": { + "type": "Integer", + "requirement": "Optional", + "setter": "setTableSize(Integer)", + "getter": "getTableSize()" + }, + "reservationId": { + "type": "String", + "requirement": "Optional", + "setter": "setReservationId(String)", + "getter": "getReservationId()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "RecipeEntity": { + "package": "com.google.android.engage.food.datamodel.RecipeEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Mutually required with other rating fields" + ] + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "author": { + "type": "String", + "requirement": "Optional", + "setter": "setAuthor(String)", + "getter": "getAuthor()" + }, + "category": { + "type": "String", + "requirement": "Optional", + "setter": "setCategory(String)", + "getter": "getCategory()" + }, + "callout": { + "type": "String", + "requirement": "Optional", + "setter": "setCallout(String)", + "getter": "getCallout()" + }, + "cookTime": { + "type": "String", + "requirement": "Optional", + "setter": "setCookTime(String)", + "getter": "getCookTime()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishFoodShoppingCarts": "PublishFoodShoppingCartsRequest", + "publishReorderCluster": "PublishReorderClusterRequest", + "publishFoodShoppingLists": "PublishFoodShoppingListsRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteFoodShoppingCartCluster": "DeleteClustersRequest", + "deleteFoodShoppingListCluster": "DeleteClustersRequest", + "deleteReorderCluster": "DeleteClustersRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "clusters": { + "FoodReorderCluster": { + "package": "com.google.android.engage.food.datamodel.FoodReorderCluster", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "FoodShoppingList": { + "package": "com.google.android.engage.food.datamodel.FoodShoppingList", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "lastUserInteractionTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastUserInteractionTimestampMillis(long)", + "getter": "getLastUserInteractionTimestampMillis()" + } + } + }, + "FoodShoppingCart": { + "package": "com.google.android.engage.food.datamodel.FoodShoppingCart", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "lastUserInteractionTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastUserInteractionTimestampMillis(long)", + "getter": "getLastUserInteractionTimestampMillis()" + } + } + } + }, + "intents": { + "ACTION_PUBLISH_FOOD_SHOPPING_CART": "com.google.android.engage.action.food.PUBLISH_FOOD_SHOPPING_CART", + "ACTION_PUBLISH_FOOD_SHOPPING_LIST": "com.google.android.engage.action.food.PUBLISH_FOOD_SHOPPING_LIST", + "ACTION_PUBLISH_REORDER_CLUSTER": "com.google.android.engage.action.food.PUBLISH_REORDER_CLUSTER" + } + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/listen.md b/play/engage-sdk-integration/references/schemas/listen.md new file mode 100644 index 0000000..96c6e91 --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/listen.md @@ -0,0 +1,842 @@ +This file defines the schema for the LISTEN vertical in the Engage SDK. + + { + "client": "com.google.android.engage.service.AppEngagePublishClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_CONTINUATION" + ], + "entities": { + "MusicAlbumEntity": { + "package": "com.google.android.engage.audio.datamodel.MusicAlbumEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "songsCount": { + "type": "Integer", + "requirement": "Optional", + "setter": "setSongsCount(int)", + "getter": "getSongsCount()" + }, + "releaseDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setReleaseDateEpochMillis(long)", + "getter": "getReleaseDateEpochMillis()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "musicLabels": { + "type": "List", + "requirement": "Optional", + "adder": "addMusicLabel(String)", + "getter": "getMusicLabels()", + "adderAll": "addMusicLabels(List)" + }, + "artists": { + "type": "List", + "requirement": "Required", + "adder": "addArtist(String)", + "getter": "getArtists()", + "adderAll": "addArtists(List)" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "musicAlbumType": { + "type": "@MusicAlbumType int", + "requirement": "Optional", + "setter": "setMusicAlbumType(@MusicAlbumType int)", + "getter": "getMusicAlbumType()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + } + } + }, + "MusicTrackEntity": { + "package": "com.google.android.engage.audio.datamodel.MusicTrackEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "album": { + "type": "String", + "requirement": "Optional", + "setter": "setAlbum(String)", + "getter": "getAlbum()" + }, + "artists": { + "type": "List", + "requirement": "Required", + "adder": "addArtist(String)", + "getter": "getArtists()", + "adderAll": "addArtists(List)" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + } + } + }, + "PodcastEpisodeEntity": { + "package": "com.google.android.engage.audio.datamodel.PodcastEpisodeEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "episodeIndex": { + "type": "Integer", + "requirement": "Optional", + "setter": "setEpisodeIndex(int)", + "getter": "getEpisodeIndex()" + }, + "podcastSeriesTitle": { + "type": "String", + "requirement": "Required", + "setter": "setPodcastSeriesTitle(String)", + "getter": "getPodcastSeriesTitle()" + }, + "productionName": { + "type": "String", + "requirement": "Required", + "setter": "setProductionName(String)", + "getter": "getProductionName()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "publishDateEpochMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setPublishDateEpochMillis(long)", + "getter": "getPublishDateEpochMillis()" + }, + "genres": { + "type": "List>", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "hosts": { + "type": "List>", + "requirement": "Optional", + "adder": "addHost(String)", + "getter": "getHosts()", + "adderAll": "addHosts(List)" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "listenNextType": { + "type": "Integer", + "requirement": "Optional", + "setter": "setListenNextType(int)", + "getter": "getListenNextType()" + }, + "videoPodcast": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setVideoPodcast(boolean)", + "getter": "isVideoPodcast()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + } + } + }, + "MusicVideoEntity": { + "package": "com.google.android.engage.audio.datamodel.MusicVideoEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "viewCount": { + "type": "String", + "requirement": "Optional", + "setter": "setViewCount(String)", + "getter": "getViewCount()" + }, + "artists": { + "type": "List", + "requirement": "Optional", + "adder": "addArtist(String)", + "getter": "getArtists()", + "adderAll": "addArtists(List)" + }, + "contentRatings": { + "type": "List>", + "requirement": "Optional", + "adder": "addContentRating(String)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + } + } + }, + "LiveRadioStationEntity": { + "package": "com.google.android.engage.audio.datamodel.LiveRadioStationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "radioFrequencyId": { + "type": "String", + "requirement": "Optional", + "setter": "setRadioFrequencyId(String)", + "getter": "getRadioFrequencyId()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "showTitle": { + "type": "String", + "requirement": "Optional", + "setter": "setShowTitle(String)", + "getter": "getShowTitle()" + }, + "hosts": { + "type": "List", + "requirement": "Optional", + "adder": "addHost(String)", + "getter": "getHosts()", + "adderAll": "addHosts(List)" + } + } + }, + "GenericAudioEntity": { + "package": "com.google.android.engage.audio.datamodel.GenericAudioEntity", + "fields": { + "entityId": { + "requirement": "Optional", + "setter": "setEntityId(String)", + "type": "String", + "getter": "getEntityId()" + }, + "name": { + "requirement": "Required", + "setter": "setName(String)", + "type": "String", + "getter": "getName()" + }, + "posterImages": { + "requirement": "Required", + "adder": "addPosterImage(Image)", + "type": "List", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "requirement": "Required", + "setter": "setActionUri(Uri)", + "type": "Uri", + "getter": "getActionUri()" + }, + "downloadedOnDevice": { + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "type": "Boolean", + "getter": "isDownloadedOnDevice()" + }, + "explicitContent": { + "requirement": "Required", + "setter": "setExplicitContent(boolean)", + "type": "Boolean", + "getter": "isExplicitContent()" + }, + "progressPercentComplete": { + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "type": "Integer", + "getter": "getProgressPercentComplete()" + }, + "lastEngagementTimeMillis": { + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "type": "Long", + "getter": "getLastEngagementTimeMillis()" + }, + "listenNextType": { + "requirement": "Optional", + "setter": "setListenNextType(@ListenNextType int)", + "type": "@ListenNextType int", + "getter": "getListenNextType()" + }, + "price": { + "requirement": "Optional", + "setter": "setPrice(Price)", + "type": "Price", + "getter": "getPrice()" + }, + "rating": { + "requirement": "Optional", + "setter": "setRating(Rating)", + "type": "Rating", + "getter": "getRating()" + }, + "callout": { + "requirement": "Optional", + "setter": "setCallout(String)", + "type": "String", + "getter": "getCallout()" + }, + "calloutFinePrint": { + "requirement": "Optional", + "setter": "setCalloutFinePrint(String)", + "type": "String", + "getter": "getCalloutFinePrint()" + }, + "displayTimeWindows": { + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "type": "List", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "type": "List>", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "isBook": { + "requirement": "Optional", + "setter": "setIsBook(boolean)", + "type": "Boolean", + "getter": "isBook()" + }, + "isTalk": { + "requirement": "Optional", + "setter": "setIsTalk(Boolean)", + "type": "Boolean", + "getter": "isTalk()" + }, + "isVideoSupported": { + "requirement": "Optional", + "setter": "setIsVideoSupported(Boolean)", + "type": "Boolean", + "getter": "isVideoSupported()" + }, + "isArtist": { + "requirement": "Optional", + "setter": "setIsArtist(Boolean)", + "type": "Boolean", + "getter": "isArtist()" + }, + "subtitles": { + "requirement": "Optional", + "adder": "addSubtitle(String)", + "type": "List", + "adderAll": "addSubtitles(List)" + } + } + }, + "PodcastSeriesEntity": { + "package": "com.google.android.engage.audio.datamodel.PodcastSeriesEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "episodeCount": { + "type": "Integer", + "requirement": "Optional", + "setter": "setEpisodeCount(int)", + "getter": "getEpisodeCount()" + }, + "productionName": { + "type": "String", + "requirement": "Optional", + "setter": "setProductionName(String)", + "getter": "getProductionName()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "genres": { + "type": "List>", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "hosts": { + "type": "List>", + "requirement": "Optional", + "adder": "addHost(String)", + "getter": "getHosts()", + "adderAll": "addHosts(List)" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + } + } + }, + "MusicArtistEntity": { + "package": "com.google.android.engage.audio.datamodel.MusicArtistEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + } + } + }, + "PlaylistEntity": { + "package": "com.google.android.engage.audio.datamodel.PlaylistEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "songsCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setSongsCount(int)", + "getter": "getSongsCount()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "explicitContent": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setExplicitContent(boolean)", + "getter": "isExplicitContent()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishContinuationCluster": "PublishContinuationClusterRequest", + "publishSubscription": "PublishSubscriptionRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteContinuationCluster": "DeleteClustersRequest", + "deleteSubscription": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/other.md b/play/engage-sdk-integration/references/schemas/other.md new file mode 100644 index 0000000..a12b69d --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/other.md @@ -0,0 +1,220 @@ +This file defines the schema for the OTHER vertical in the Engage SDK. + + { + "client": "com.google.android.engage.service.AppEngagePublishClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED" + ], + "entities": { + "ArticleEntity": { + "package": "com.google.android.engage.common.datamodel.ArticleEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitleList": { + "type": "List", + "requirement": "Optional" + }, + "badgeList": { + "type": "List", + "requirement": "Optional" + }, + "contentCategoryList": { + "type": "List", + "requirement": "Optional" + }, + "lastEngagementTime": { + "type": "Long", + "requirement": "Optional", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "actionUri": { + "requirement": "Required", + "setter": "setActionUri(Uri)", + "type": "Uri", + "getter": "getActionUri()" + }, + "subtitles": { + "requirement": "Optional", + "adder": "addSubtitle(String)", + "type": "List", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "requirement": "Optional", + "adder": "addBadge(Badge)", + "type": "List", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "type": "List<@EligibleContentCategory int>", + "adderAll": "addContentCategories(List)" + }, + "progressPercentage": { + "requirement": "Required", + "setter": "setProgressPercentage(int)", + "type": "Integer", + "getter": "getProgressPercentage()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastEngagementTimestampMillis": { + "requirement": "Required", + "setter": "setLastEngagementTimestampMillis(long)", + "type": "Long", + "getter": "getLastEngagementTimestampMillis()" + }, + "source": { + "requirement": "Optional", + "setter": "setSource(Badge)", + "type": "Badge", + "getter": "getSource()" + }, + "lastContentPublishTimestampMillis": { + "requirement": "Optional", + "setter": "setLastContentPublishTimestampMillis(Long)", + "type": "Long", + "getter": "getLastContentPublishTimestampMillis()" + }, + "allDisplayTimeWindows": { + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "type": "List>", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "GenericFeaturedEntity": { + "package": "com.google.android.engage.common.datamodel.GenericFeaturedEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitleList": { + "type": "List", + "requirement": "Optional" + }, + "badgeList": { + "type": "List", + "requirement": "Optional" + }, + "contentCategoryList": { + "type": "List", + "requirement": "Optional" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "subtitles": { + "requirement": "Optional", + "adder": "addSubtitle(String)", + "type": "List", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "requirement": "Optional", + "adder": "addBadge(Badge)", + "type": "List", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "type": "List<@EligibleContentCategory int>", + "adderAll": "addContentCategories(List)" + }, + "allDisplayTimeWindows": { + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "type": "List>", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishContinuationCluster": "PublishContinuationClusterRequest", + "publishSubscription": "PublishSubscriptionRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteContinuationCluster": "DeleteClustersRequest", + "deleteSubscription": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/read.md b/play/engage-sdk-integration/references/schemas/read.md new file mode 100644 index 0000000..698d707 --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/read.md @@ -0,0 +1,412 @@ +This file defines the schema for the READ vertical in the Engage SDK. + + { + "client": "com.google.android.engage.service.AppEngagePublishClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_CONTINUATION" + ], + "entities": { + "EbookEntity": { + "package": "com.google.android.engage.books.datamodel.EbookEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "authors": { + "type": "List", + "requirement": "Required", + "adder": "addAuthor(String)", + "getter": "getAuthors()", + "adderAll": "addAuthors(List)" + }, + "publishDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setPublishDateEpochMillis(long)", + "getter": "getPublishDateEpochMillis()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "pageCount": { + "type": "Integer", + "requirement": "Optional", + "setter": "setPageCount(int)", + "getter": "getPageCount()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "seriesName": { + "type": "String", + "requirement": "Optional", + "setter": "setSeriesName(String)", + "getter": "getSeriesName()" + }, + "seriesUnitIndex": { + "type": "Integer", + "requirement": "Optional", + "setter": "setSeriesUnitIndex(Integer)", + "getter": "getSeriesUnitIndex()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Optional", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "continueBookType": { + "type": "Integer", + "requirement": "Optional", + "setter": "setContinueBookType(int)", + "getter": "getContinueBookType()", + "requiredFor": [ + "ContinuationCluster" + ] + } + } + }, + "BookSeriesEntity": { + "package": "com.google.android.engage.books.datamodel.BookSeriesEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "bookCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setBookCount(int)", + "getter": "getBookCount()" + }, + "authors": { + "type": "List", + "requirement": "Required", + "adder": "addAuthor(String)", + "getter": "getAuthors()", + "adderAll": "addAuthors(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "genres": { + "type": "List>", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Optional", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "continueBookType": { + "type": "Integer", + "requirement": "Optional", + "setter": "setContinueBookType(int)", + "getter": "getContinueBookType()", + "requiredFor": [ + "ContinuationCluster" + ] + } + } + }, + "AudiobookEntity": { + "package": "com.google.android.engage.books.datamodel.AudiobookEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Optional", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "authors": { + "type": "List", + "requirement": "Required", + "adder": "addAuthor(String)", + "getter": "getAuthors()", + "adderAll": "addAuthors(List)" + }, + "narrators": { + "type": "List", + "requirement": "Required", + "adder": "addNarrator(String)", + "getter": "getNarrators()", + "adderAll": "addNarrators(List)" + }, + "publishDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setPublishDateEpochMillis(long)", + "getter": "getPublishDateEpochMillis()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "seriesName": { + "type": "String", + "requirement": "Optional", + "setter": "setSeriesName(String)", + "getter": "getSeriesName()" + }, + "seriesUnitIndex": { + "type": "Integer", + "requirement": "Optional", + "setter": "setSeriesUnitIndex(Integer)", + "getter": "getSeriesUnitIndex()" + }, + "genres": { + "type": "List>", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()" + }, + "progressPercentComplete": { + "type": "Integer", + "requirement": "Optional", + "setter": "setProgressPercentComplete(int)", + "getter": "getProgressPercentComplete()" + }, + "continueBookType": { + "type": "Integer", + "requirement": "Optional", + "setter": "setContinueBookType(int)", + "getter": "getContinueBookType()" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishContinuationCluster": "PublishContinuationClusterRequest", + "publishSubscription": "PublishSubscriptionRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteContinuationCluster": "DeleteClustersRequest", + "deleteSubscription": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/shopping.md b/play/engage-sdk-integration/references/schemas/shopping.md new file mode 100644 index 0000000..e54edce --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/shopping.md @@ -0,0 +1,357 @@ +This file defines the schema for the SHOPPING vertical in the Engage SDK. + + { + "client": "com.google.android.engage.shopping.service.AppEngageShoppingClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_SHOPPING_CART", + "TYPE_SHOPPING_LIST", + "TYPE_SHOPPING_REORDER", + "TYPE_SHOPPING_ORDER_TRACKING" + ], + "entities": { + "ShoppingEntity": { + "package": "com.google.android.engage.shopping.datamodel.ShoppingEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "callout": { + "type": "String", + "requirement": "Optional", + "setter": "setCallout(String)", + "getter": "getCallout()" + }, + "calloutFinePrint": { + "type": "String", + "requirement": "Optional", + "setter": "setCalloutFinePrint(String)", + "getter": "getCalloutFinePrint()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Required if strikethrough price is provided", + "Mutually required with other rating fields" + ] + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishShoppingCart": "PublishShoppingCartClusterRequest", + "publishShoppingLists": "PublishShoppingListsRequest", + "publishShoppingReorderCluster": "PublishShoppingReorderClusterRequest", + "publishShoppingOrderTrackingCluster": "PublishShoppingOrderTrackingClusterRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteShoppingCartCluster": "DeleteClustersRequest", + "deleteShoppingListCluster": "DeleteClustersRequest", + "deleteReorderCluster": "DeleteClustersRequest", + "deleteShoppingOrderTrackingCluster": "DeleteClustersRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "clusters": { + "ShoppingList": { + "package": "com.google.android.engage.shopping.datamodel.ShoppingList", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "lastUserInteractionTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastUserInteractionTimestampMillis(long)", + "getter": "getLastUserInteractionTimestampMillis()" + } + } + }, + "ShoppingReorderCluster": { + "package": "com.google.android.engage.shopping.datamodel.ShoppingReorderCluster", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "ShoppingCart": { + "package": "com.google.android.engage.shopping.datamodel.ShoppingCart", + "fields": { + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "actionText": { + "type": "String", + "requirement": "Optional", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "itemLabels": { + "type": "List>", + "requirement": "Optional", + "adder": "addItemLabel(String)", + "getter": "getItemLabels()", + "adderAll": "addItemLabels(List)" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(int)", + "getter": "getNumberOfItems()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "lastUserInteractionTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastUserInteractionTimestampMillis(long)", + "getter": "getLastUserInteractionTimestampMillis()" + } + } + }, + "ShoppingOrderTrackingCluster": { + "package": "com.google.android.engage.shopping.datamodel.ShoppingOrderTrackingCluster", + "fields": { + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "posterImages": { + "type": "List", + "requirement": "Optional", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "status": { + "type": "String", + "requirement": "Required", + "setter": "setStatus(String)", + "getter": "getStatus()" + }, + "orderTime": { + "type": "Long", + "requirement": "Required", + "setter": "setOrderTime(long)", + "getter": "getOrderTime()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "orderReadyTimeWindow": { + "type": "OrderReadyTimeWindow", + "requirement": "Optional", + "setter": "setOrderReadyTimeWindow(OrderReadyTimeWindow)", + "getter": "getOrderReadyTimeWindow()" + }, + "numberOfItems": { + "type": "Integer", + "requirement": "Optional", + "setter": "setNumberOfItems(Integer)", + "getter": "getNumberOfItems()" + }, + "orderDescription": { + "type": "String", + "requirement": "Optional", + "setter": "setOrderDescription(String)", + "getter": "getOrderDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "orderValue": { + "type": "Price", + "requirement": "Optional", + "setter": "setOrderValue(Price)", + "getter": "getOrderValue()" + }, + "shoppingOrderType": { + "type": "@ShoppingOrderType int", + "requirement": "Required", + "setter": "setShoppingOrderType(@ShoppingOrderType int)", + "getter": "getShoppingOrderType()" + }, + "trackingId": { + "type": "String", + "requirement": "Optional", + "setter": "setTrackingId(String)", + "getter": "getTrackingId()" + } + } + } + }, + "intents": { + "ACTION_PUBLISH_SHOPPING_CART": "com.google.android.engage.action.shopping.PUBLISH_SHOPPING_CART", + "ACTION_PUBLISH_SHOPPING_LIST": "com.google.android.engage.action.shopping.PUBLISH_SHOPPING_LIST", + "ACTION_PUBLISH_REORDER_CLUSTER": "com.google.android.engage.action.shopping.PUBLISH_REORDER_CLUSTER", + "ACTION_PUBLISH_ORDER_TRACKING_CLUSTER": "com.google.android.engage.action.shopping.PUBLISH_ORDER_TRACKING_CLUSTER" + } + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/social.md b/play/engage-sdk-integration/references/schemas/social.md new file mode 100644 index 0000000..5aa01bf --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/social.md @@ -0,0 +1,228 @@ +This file defines the schema for the SOCIAL vertical in the Engage SDK. + + { + "client": "com.google.android.engage.social.service.AppEngageSocialClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED" + ], + "entities": { + "SocialPostEntity": { + "package": "com.google.android.engage.social.datamodel.SocialPostEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "genericPost": { + "type": "GenericPost", + "requirement": "Required", + "setter": "setGenericPost(GenericPost)", + "getter": "getGenericPost()" + }, + "profile": { + "type": "Profile", + "requirement": "Optional", + "setter": "setProfile(Profile)", + "getter": "getProfile()" + }, + "interactions": { + "type": "List", + "requirement": "Optional", + "adder": "addInteraction(Interaction)", + "getter": "getInteractions()" + }, + "allInteractions": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllInteraction(Interaction)", + "adderAll": "addAllInteraction(List)" + } + } + }, + "PortraitMediaEntity": { + "package": "com.google.android.engage.social.datamodel.PortraitMediaEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "portraitMediaPost": { + "type": "PortraitMediaPost", + "requirement": "Required", + "setter": "setPortraitMediaPost(PortraitMediaPost)", + "getter": "getPortraitMediaPost()" + }, + "profile": { + "type": "Profile", + "requirement": "Optional", + "setter": "setProfile(Profile)", + "getter": "getProfile()" + }, + "interactions": { + "type": "List>", + "requirement": "Optional", + "adder": "addInteractions(Interaction)", + "getter": "getInteractions()", + "adderAll": "addInteractions(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "platformSpecificPlaybackUris": { + "type": "List<@NonNull PlatformSpecificUri>", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(@NonNull PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "commentsSummary": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setCommentsSummary(@NonNull String)", + "getter": "getCommentsSummary()" + }, + "interaction": { + "requirement": "Optional", + "setter": "setInteraction(Interaction)", + "type": "Interaction", + "getter": "getInteraction()" + } + } + }, + "PersonEntity": { + "package": "com.google.android.engage.social.datamodel.PersonEntity", + "fields": { + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "profile": { + "type": "Profile", + "requirement": "Required", + "setter": "setProfile(Profile)", + "getter": "getProfile()" + }, + "headerImage": { + "type": "Image", + "requirement": "Optional", + "setter": "setHeaderImage(Image)", + "getter": "getHeaderImage()" + }, + "popularity": { + "type": "Popularity", + "requirement": "Optional", + "setter": "setPopularity(Popularity)", + "getter": "getPopularity()" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "locations": { + "type": "Address", + "requirement": "Optional" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "location": { + "requirement": "Optional", + "setter": "setLocation(Address)", + "type": "Address", + "getter": "getLocation()" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteUserManagementCluster": "DeleteClustersRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/travel.md b/play/engage-sdk-integration/references/schemas/travel.md new file mode 100644 index 0000000..0cf70cc --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/travel.md @@ -0,0 +1,923 @@ +This file defines the schema for the TRAVEL vertical in the Engage SDK. + + { + "client": "com.google.android.engage.travel.service.AppEngageTravelClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_RESERVATION", + "TYPE_CONTINUE_SEARCH" + ], + "entities": { + "EventEntity": { + "package": "com.google.android.engage.travel.datamodel.EventEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "title": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setTitle(@NonNull String)", + "getter": "getTitle()" + }, + "startTime": { + "type": "Long", + "requirement": "Required", + "setter": "setStartTime(Long)", + "getter": "getStartTime()" + }, + "localizedStartTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedStartTime(LocalizedTimestamp)", + "getter": "getLocalizedStartTime()" + }, + "eventMode": { + "type": "@EventMode int", + "requirement": "Required", + "setter": "setEventMode(@EventMode int)", + "getter": "getEventMode()" + }, + "locations": { + "type": "Address", + "requirement": "Required" + }, + "endTime": { + "type": "Long", + "requirement": "Optional", + "setter": "setEndTime(Long)", + "getter": "getEndTime()" + }, + "localizedEndTime": { + "type": "LocalizedTimestamp", + "requirement": "Optional", + "setter": "setLocalizedEndTime(LocalizedTimestamp)", + "getter": "getLocalizedEndTime()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "location": { + "requirement": "Required", + "setter": "setLocation(Address)", + "type": "Address", + "getter": "getLocation()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + } + } + }, + "LodgingEntity": { + "package": "com.google.android.engage.travel.datamodel.LodgingEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "locations": { + "type": "Address", + "requirement": "Required" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "availabilityTimeWindow": { + "type": "AvailabilityTimeWindow", + "requirement": "Optional", + "setter": "setAvailabilityTimeWindow(AvailabilityTimeWindow)", + "getter": "getAvailabilityTimeWindow()" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "location": { + "requirement": "Required", + "setter": "setLocation(Address)", + "type": "Address", + "getter": "getLocation()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + } + } + }, + "PointOfInterestEntity": { + "package": "com.google.android.engage.travel.datamodel.PointOfInterestEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionLinkUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionLinkUri(Uri)", + "getter": "getActionLinkUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "locations": { + "type": "Address", + "requirement": "Required" + }, + "availabilityTimeWindow": { + "type": "AvailabilityTimeWindow", + "requirement": "Optional", + "setter": "setAvailabilityTimeWindow(AvailabilityTimeWindow)", + "getter": "getAvailabilityTimeWindow()" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "lastEngagementTime": { + "type": "Instant", + "requirement": "Optional", + "setter": "setLastEngagementTime(Instant)", + "getter": "getLastEngagementTime()", + "requiredFor": [ + "ContinueSearchCluster" + ] + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + }, + "location": { + "requirement": "Required", + "setter": "setLocation(Address)", + "type": "Address", + "getter": "getLocation()", + "requiredFor": [ + "RecommendationCluster" + ] + } + } + }, + "LodgingReservationEntity": { + "package": "com.google.android.engage.travel.datamodel.LodgingReservationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "address": { + "type": "Address", + "requirement": "Required", + "setter": "setAddress(Address)", + "getter": "getAddress()" + }, + "checkInTime": { + "type": "Long", + "requirement": "Required", + "setter": "setCheckInTime(long)", + "getter": "getCheckInTime()" + }, + "localizedCheckInTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedCheckInTime(LocalizedTimestamp)", + "getter": "getLocalizedCheckInTime()" + }, + "checkOutTime": { + "type": "Long", + "requirement": "Required", + "setter": "setCheckOutTime(long)", + "getter": "getCheckOutTime()" + }, + "localizedCheckOutTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedCheckOutTime(LocalizedTimestamp)", + "getter": "getLocalizedCheckOutTime()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Required if strikethrough price is provided", + "Mutually required with other rating fields" + ] + }, + "reservationId": { + "type": "String", + "requirement": "Optional", + "setter": "setReservationId(String)", + "getter": "getReservationId()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "EventReservationEntity": { + "package": "com.google.android.engage.travel.datamodel.EventReservationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "startTime": { + "type": "Long", + "requirement": "Required", + "setter": "setStartTime(Long)", + "getter": "getStartTime()" + }, + "localizedStartTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedStartTime(LocalizedTimestamp)", + "getter": "getLocalizedStartTime()" + }, + "eventMode": { + "type": "@EventMode int", + "requirement": "Required", + "setter": "setEventMode(@EventMode int)", + "getter": "getEventMode()" + }, + "location": { + "type": "Address", + "requirement": "Required", + "setter": "setLocation(Address)", + "getter": "getLocation()" + }, + "endTime": { + "type": "Long", + "requirement": "Optional", + "setter": "setEndTime(Long)", + "getter": "getEndTime()" + }, + "localizedEndTime": { + "type": "LocalizedTimestamp", + "requirement": "Optional", + "setter": "setLocalizedEndTime(LocalizedTimestamp)", + "getter": "getLocalizedEndTime()" + }, + "serviceProvider": { + "type": "ServiceProvider", + "requirement": "Optional", + "setter": "setServiceProvider(ServiceProvider)", + "getter": "getServiceProvider()" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "rating": { + "type": "Rating", + "requirement": "Optional", + "setter": "setRating(Rating)", + "getter": "getRating()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "reservationId": { + "type": "String", + "requirement": "Optional", + "setter": "setReservationId(String)", + "getter": "getReservationId()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "TransportationReservationEntity": { + "package": "com.google.android.engage.travel.datamodel.TransportationReservationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "departureTime": { + "type": "Long", + "requirement": "Required", + "setter": "setDepartureTime(Long)", + "getter": "getDepartureTime()" + }, + "localizedDepartureTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedDepartureTime(LocalizedTimestamp)", + "getter": "getLocalizedDepartureTime()" + }, + "arrivalTime": { + "type": "Long", + "requirement": "Required", + "setter": "setArrivalTime(Long)", + "getter": "getArrivalTime()" + }, + "localizedArrivalTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedArrivalTime(LocalizedTimestamp)", + "getter": "getLocalizedArrivalTime()" + }, + "transportationType": { + "type": "@TransportationType int", + "requirement": "Required", + "setter": "setTransportationType(@TransportationType int)", + "getter": "getTransportationType()" + }, + "departureLocation": { + "type": "Address", + "requirement": "Optional", + "setter": "setDepartureLocation(Address)", + "getter": "getDepartureLocation()" + }, + "arrivalLocation": { + "type": "Address", + "requirement": "Optional", + "setter": "setArrivalLocation(Address)", + "getter": "getArrivalLocation()" + }, + "serviceProvider": { + "type": "ServiceProvider", + "requirement": "Optional", + "setter": "setServiceProvider(ServiceProvider)", + "getter": "getServiceProvider()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "transportationNumber": { + "type": "String", + "requirement": "Optional", + "setter": "setTransportationNumber(String)", + "getter": "getTransportationNumber()" + }, + "boardingTime": { + "type": "Long", + "requirement": "Optional", + "setter": "setBoardingTime(Long)", + "getter": "getBoardingTime()" + }, + "localizedBoardingTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedBoardingTime(LocalizedTimestamp)", + "getter": "getLocalizedBoardingTime()" + }, + "reservationId": { + "type": "String", + "requirement": "Optional", + "setter": "setReservationId(String)", + "getter": "getReservationId()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "VehicleRentalReservationEntity": { + "package": "com.google.android.engage.travel.datamodel.VehicleRentalReservationEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Required", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "pickupTime": { + "type": "Long", + "requirement": "Required", + "setter": "setPickupTime(Long)", + "getter": "getPickupTime()" + }, + "localizedPickupTime": { + "type": "LocalizedTimestamp", + "requirement": "Required", + "setter": "setLocalizedPickupTime(LocalizedTimestamp)", + "getter": "getLocalizedPickupTime()" + }, + "returnTime": { + "type": "Long", + "requirement": "Optional", + "setter": "setReturnTime(Long)", + "getter": "getReturnTime()" + }, + "localizedReturnTime": { + "type": "LocalizedTimestamp", + "requirement": "Optional", + "setter": "setLocalizedReturnTime(LocalizedTimestamp)", + "getter": "getLocalizedReturnTime()" + }, + "pickupAddress": { + "type": "Address", + "requirement": "Optional", + "setter": "setPickupAddress(Address)", + "getter": "getPickupAddress()" + }, + "returnAddress": { + "type": "Address", + "requirement": "Optional", + "setter": "setReturnAddress(Address)", + "getter": "getReturnAddress()" + }, + "serviceProvider": { + "type": "ServiceProvider", + "requirement": "Optional", + "setter": "setServiceProvider(ServiceProvider)", + "getter": "getServiceProvider()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()", + "requiredFor": [ + "Required if strikethrough price is provided" + ] + }, + "priceCallout": { + "type": "String", + "requirement": "Optional", + "setter": "setPriceCallout(String)", + "getter": "getPriceCallout()" + }, + "confirmationId": { + "type": "String", + "requirement": "Optional", + "setter": "setConfirmationId(String)", + "getter": "getConfirmationId()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "publishContinueSearchCluster": "PublishContinueSearchClusterRequest", + "publishReservationCluster": "PublishReservationClusterRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest", + "deleteContinueSearchCluster": "DeleteClustersRequest", + "deleteReservationCluster": "DeleteClustersRequest" + }, + "clusters": { + "ContinueSearchCluster": { + "package": "com.google.android.engage.travel.datamodel.ContinueSearchCluster", + "fields": { + "pointOfInterestEntities": { + "type": "List", + "requirement": "Required", + "adder": "addPointOfInterestEntity(PointOfInterestEntity)" + } + } + }, + "ReservationCluster": { + "package": "com.google.android.engage.travel.datamodel.ReservationCluster", + "fields": { + "lodgingReservationEntities": { + "type": "List", + "requirement": "Optional", + "adder": "addLodgingReservationEntity(LodgingReservationEntity)" + }, + "vehicleRentalReservationEntities": { + "type": "List", + "requirement": "Optional", + "adder": "addVehicleRentalReservationEntity(VehicleRentalReservationEntity)" + }, + "transportationReservationEntities": { + "type": "List", + "requirement": "Optional", + "adder": "addTransportationReservationEntity(TransportationReservationEntity)" + }, + "eventReservationEntities": { + "type": "List", + "requirement": "Optional", + "adder": "addEventReservationEntity(EventReservationEntity)" + }, + "restaurantReservationEntities": { + "type": "List", + "requirement": "Optional", + "adder": "addRestaurantReservationEntity(RestaurantReservationEntity)" + } + } + } + }, + "intents": { + "ACTION_PUBLISH_CONTINUE_SEARCH_CLUSTER": "com.google.android.engage.action.travel.PUBLISH_CONTINUE_SEARCH", + "ACTION_PUBLISH_RESERVATION_CLUSTER": "com.google.android.engage.action.travel.PUBLISH_RESERVATION" + } + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/tv.md b/play/engage-sdk-integration/references/schemas/tv.md new file mode 100644 index 0000000..4d2f13f --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/tv.md @@ -0,0 +1,1226 @@ +This file defines the schema for the TV vertical in the Engage SDK. + + { + "client": "com.google.android.engage.service.AppEngagePublishClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_CONTINUATION", + "TYPE_SUBSCRIPTION", + "TYPE_ENGAGEMENT" + ], + "entities": { + "MovieEntity": { + "package": "com.google.android.engage.video.datamodel.MovieEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "releaseDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setReleaseDateEpochMillis(long)", + "getter": "getReleaseDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "contentRatingsLegacies": { + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "type": "List>", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "TvShowEntity": { + "package": "com.google.android.engage.video.datamodel.TvShowEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "firstEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setFirstEpisodeAirDateEpochMillis(long)", + "getter": "getFirstEpisodeAirDateEpochMillis()" + }, + "latestEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLatestEpisodeAirDateEpochMillis(long)", + "getter": "getLatestEpisodeAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "seasonCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setSeasonCount(int)", + "getter": "getSeasonCount()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Required", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "contentRatingsLegacies": { + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "type": "List>", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "TvSeasonEntity": { + "package": "com.google.android.engage.video.datamodel.TvSeasonEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "seasonNumber": { + "type": "Integer", + "requirement": "Required", + "setter": "setSeasonNumber(int)" + }, + "seasonDisplayNumber": { + "type": "String", + "requirement": "Required", + "setter": "setSeasonDisplayNumber(String)", + "getter": "getSeasonDisplayNumber()" + }, + "firstEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setFirstEpisodeAirDateEpochMillis(long)", + "getter": "getFirstEpisodeAirDateEpochMillis()" + }, + "latestEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLatestEpisodeAirDateEpochMillis(long)", + "getter": "getLatestEpisodeAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "episodeCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setEpisodeCount(int)", + "getter": "getEpisodeCount()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "contentRatingsLegacies": { + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "type": "List>", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + } + } + }, + "TvEpisodeEntity": { + "package": "com.google.android.engage.video.datamodel.TvEpisodeEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "episodeNumber": { + "type": "Integer", + "requirement": "Required", + "setter": "setEpisodeNumber(int)" + }, + "episodeDisplayNumber": { + "type": "String", + "requirement": "Required", + "setter": "setEpisodeDisplayNumber(String)", + "getter": "getEpisodeDisplayNumber()" + }, + "airDateEpochMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setAirDateEpochMillis(long)", + "getter": "getAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "seasonNumber": { + "type": "String", + "requirement": "Optional", + "setter": "setSeasonNumber(String)", + "getter": "getSeasonNumber()" + }, + "seasonTitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSeasonTitle(String)", + "getter": "getSeasonTitle()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "showTitle": { + "type": "String", + "requirement": "Optional", + "setter": "setShowTitle(String)", + "getter": "getShowTitle()" + }, + "isNextEpisodeAvailable": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setIsNextEpisodeAvailable(boolean)", + "getter": "getIsNextEpisodeAvailable()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "contentRatingsLegacies": { + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "type": "List>", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "MediaActionFeedEntity": { + "package": "com.google.android.engage.video.datamodel.MediaActionFeedEntity", + "fields": { + "dataFeedElementId": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setDataFeedElementId(@NonNull String)", + "getter": "getDataFeedElementId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "posterImages": { + "type": "List>", + "requirement": "Optional", + "adder": "addPosterImage(@NonNull Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveTvProgramEntity": { + "package": "com.google.android.engage.video.datamodel.LiveTvProgramEntity", + "fields": { + "channelId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setChannelId(@NonNull String)", + "getter": "getChannelId()" + }, + "channelName": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setChannelName(@NonNull String)", + "getter": "getChannelName()" + }, + "channelLogoImage": { + "type": "@NonNull Image", + "requirement": "Optional", + "setter": "setChannelLogoImage(@NonNull Image)", + "getter": "getChannelLogoImage()" + }, + "platformSpecificPlaybackUris": { + "type": "List<@NonNull PlatformSpecificUri>", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(@NonNull PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "genres": { + "type": "List<@NonNull String>", + "requirement": "Optional", + "adder": "addGenre(@NonNull String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "availabilityTimeWindows": { + "type": "List<@NonNull DisplayTimeWindow>", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(@NonNull DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()", + "adderAll": "addAvailabilityTimeWindows(List)" + }, + "posterImages": { + "type": "List<@NonNull Image>", + "requirement": "Optional", + "adder": "addPosterImage(@NonNull Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "entityId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setEntityId(@NonNull String)", + "getter": "getEntityId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveStreamingVideoEntity": { + "package": "com.google.android.engage.video.datamodel.LiveStreamingVideoEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "startTimeEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setStartTimeEpochMillis(long)", + "getter": "getStartTimeEpochMillis()" + }, + "endTimeEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setEndTimeEpochMillis(long)", + "getter": "getEndTimeEpochMillis()" + }, + "broadcaster": { + "type": "String", + "requirement": "Required", + "setter": "setBroadcaster(String)", + "getter": "getBroadcaster()" + }, + "viewCount": { + "type": "String", + "requirement": "Optional", + "setter": "setViewCount(String)", + "getter": "getViewCount()" + }, + "broadcasterIcon": { + "type": "Image", + "requirement": "Optional", + "setter": "setBroadcasterIcon(Image)", + "getter": "getBroadcasterIcon()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "genres": { + "type": "List<@NonNull String>", + "requirement": "Optional", + "adder": "addGenre(@NonNull String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveTvChannelEntity": { + "package": "com.google.android.engage.video.datamodel.LiveTvChannelEntity", + "fields": { + "entityId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setEntityId(@NonNull String)", + "getter": "getEntityId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "logoImage": { + "type": "@NonNull Image", + "requirement": "Optional", + "setter": "setLogoImage(@NonNull Image)", + "getter": "getLogoImage()" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "platformSpecificPlaybackUris": { + "type": "List<@NonNull PlatformSpecificUri>", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(@NonNull PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "VideoClipEntity": { + "package": "com.google.android.engage.video.datamodel.VideoClipEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "createdTimeEpochMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setCreatedTimeEpochMillis(long)", + "getter": "getCreatedTimeEpochMillis()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "creator": { + "type": "String", + "requirement": "Required", + "setter": "setCreator(String)", + "getter": "getCreator()" + }, + "creatorImage": { + "type": "Image", + "requirement": "Optional", + "setter": "setCreatorImage(Image)", + "getter": "getCreatorImage()" + }, + "viewCount": { + "type": "String", + "requirement": "Optional", + "setter": "setViewCount(String)", + "getter": "getViewCount()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "RatingSystem": { + "package": "com.google.android.engage.video.datamodel.RatingSystem", + "fields": { + "rating": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setRating(@NonNull String)", + "getter": "getRating()" + }, + "agencyName": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setAgencyName(@NonNull String)", + "getter": "getAgencyName()" + } + } + }, + "PlatformSpecificUri": { + "package": "com.google.android.engage.common.datamodel.PlatformSpecificUri", + "fields": { + "platformType": { + "type": "@PlatformType int", + "requirement": "Required", + "setter": "setPlatformType(@PlatformType int)", + "getter": "getPlatformType()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + } + } + }, + "SubscriptionEntitlement": { + "package": "com.google.android.engage.common.datamodel.SubscriptionEntitlement", + "fields": { + "entitlementId": { + "type": "String", + "requirement": "Required", + "setter": "setEntitlementId(String)", + "getter": "getEntitlementId()" + }, + "displayName": { + "type": "String", + "requirement": "Optional", + "setter": "setDisplayName(String)", + "getter": "getDisplayName()" + }, + "expirationTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setExpirationTimeMillis(long)", + "getter": "getExpirationTimeMillis()" + } + } + }, + "BundledSubscription": { + "package": "com.google.android.engage.common.datamodel.BundledSubscription", + "fields": { + "bundledSubscriptionProviderPackageName": { + "type": "String", + "requirement": "Optional", + "setter": "setBundledSubscriptionProviderPackageName(String)", + "getter": "getBundledSubscriptionProviderPackageName()" + }, + "subscriptionType": { + "type": "@SubscriptionType int", + "requirement": "Optional", + "setter": "setSubscriptionType(@SubscriptionType int)", + "getter": "getSubscriptionType()" + }, + "expirationTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setExpirationTimeMillis(long)", + "getter": "getExpirationTimeMillis()" + }, + "entitlements": { + "type": "List", + "requirement": "Optional", + "adder": "addEntitlement(SubscriptionEntitlement)", + "getter": "getEntitlements()", + "adderAll": "addEntitlements(List)" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishContinuationCluster": "PublishContinuationClusterRequest", + "publishSubscription": "PublishSubscriptionRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteContinuationCluster": "DeleteClustersRequest", + "deleteSubscription": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/watch.md b/play/engage-sdk-integration/references/schemas/watch.md new file mode 100644 index 0000000..8e52fcc --- /dev/null +++ b/play/engage-sdk-integration/references/schemas/watch.md @@ -0,0 +1,1137 @@ +This file defines the schema for the WATCH vertical in the Engage SDK. + + { + "client": "com.google.android.engage.service.AppEngagePublishClient", + "clusterTypes": [ + "TYPE_RECOMMENDATION", + "TYPE_FEATURED", + "TYPE_CONTINUATION" + ], + "entities": { + "MovieEntity": { + "package": "com.google.android.engage.video.datamodel.MovieEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "releaseDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setReleaseDateEpochMillis(long)", + "getter": "getReleaseDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "contentRatingsLegacies": { + "type": "List>", + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "TvShowEntity": { + "package": "com.google.android.engage.video.datamodel.TvShowEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "firstEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setFirstEpisodeAirDateEpochMillis(long)", + "getter": "getFirstEpisodeAirDateEpochMillis()" + }, + "latestEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLatestEpisodeAirDateEpochMillis(long)", + "getter": "getLatestEpisodeAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "seasonCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setSeasonCount(int)", + "getter": "getSeasonCount()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "contentRatingsLegacies": { + "type": "List>", + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Required", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "TvSeasonEntity": { + "package": "com.google.android.engage.video.datamodel.TvSeasonEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "seasonNumber": { + "type": "Integer", + "requirement": "Required", + "setter": "setSeasonNumber(int)" + }, + "seasonDisplayNumber": { + "type": "String", + "requirement": "Required", + "setter": "setSeasonDisplayNumber(String)", + "getter": "getSeasonDisplayNumber()" + }, + "firstEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setFirstEpisodeAirDateEpochMillis(long)", + "getter": "getFirstEpisodeAirDateEpochMillis()" + }, + "latestEpisodeAirDateEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLatestEpisodeAirDateEpochMillis(long)", + "getter": "getLatestEpisodeAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "episodeCount": { + "type": "Integer", + "requirement": "Required", + "setter": "setEpisodeCount(int)", + "getter": "getEpisodeCount()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "contentRatingsLegacies": { + "type": "List>", + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + } + } + }, + "TvEpisodeEntity": { + "package": "com.google.android.engage.video.datamodel.TvEpisodeEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "infoPageUri": { + "type": "Uri", + "requirement": "Optional", + "setter": "setInfoPageUri(Uri)", + "getter": "getInfoPageUri()" + }, + "episodeNumber": { + "type": "Integer", + "requirement": "Required", + "setter": "setEpisodeNumber(int)" + }, + "episodeDisplayNumber": { + "type": "String", + "requirement": "Required", + "setter": "setEpisodeDisplayNumber(String)", + "getter": "getEpisodeDisplayNumber()" + }, + "airDateEpochMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setAirDateEpochMillis(long)", + "getter": "getAirDateEpochMillis()" + }, + "availability": { + "type": "@ContentAvailability int", + "requirement": "Required", + "setter": "setAvailability(@ContentAvailability int)", + "getter": "getAvailability()" + }, + "genres": { + "type": "List", + "requirement": "Optional", + "adder": "addGenre(String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List", + "requirement": "Required", + "adder": "addContentRating(RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "contentRatingsLegacies": { + "type": "List>", + "requirement": "Required", + "adder": "addContentRatingsLegacy(String)", + "getter": "getContentRatingsLegacy()", + "adderAll": "addContentRatingsLegacy(List)" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "seasonNumber": { + "type": "String", + "requirement": "Optional", + "setter": "setSeasonNumber(String)", + "getter": "getSeasonNumber()" + }, + "seasonTitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSeasonTitle(String)", + "getter": "getSeasonTitle()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "price": { + "type": "Price", + "requirement": "Optional", + "setter": "setPrice(Price)", + "getter": "getPrice()" + }, + "showTitle": { + "type": "String", + "requirement": "Optional", + "setter": "setShowTitle(String)", + "getter": "getShowTitle()" + }, + "isNextEpisodeAvailable": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setIsNextEpisodeAvailable(boolean)", + "getter": "getIsNextEpisodeAvailable()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "MediaActionFeedEntity": { + "package": "com.google.android.engage.video.datamodel.MediaActionFeedEntity", + "fields": { + "dataFeedElementId": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setDataFeedElementId(@NonNull String)", + "getter": "getDataFeedElementId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "posterImages": { + "type": "List>", + "requirement": "Optional", + "adder": "addPosterImage(@NonNull Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveTvProgramEntity": { + "package": "com.google.android.engage.video.datamodel.LiveTvProgramEntity", + "fields": { + "channelId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setChannelId(@NonNull String)", + "getter": "getChannelId()" + }, + "channelName": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setChannelName(@NonNull String)", + "getter": "getChannelName()" + }, + "channelLogoImage": { + "type": "@NonNull Image", + "requirement": "Optional", + "setter": "setChannelLogoImage(@NonNull Image)", + "getter": "getChannelLogoImage()" + }, + "platformSpecificPlaybackUris": { + "type": "List<@NonNull PlatformSpecificUri>", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(@NonNull PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "genres": { + "type": "List<@NonNull String>", + "requirement": "Optional", + "adder": "addGenre(@NonNull String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "availabilityTimeWindows": { + "type": "List<@NonNull DisplayTimeWindow>", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(@NonNull DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()", + "adderAll": "addAvailabilityTimeWindows(List)" + }, + "posterImages": { + "type": "List<@NonNull Image>", + "requirement": "Optional", + "adder": "addPosterImage(@NonNull Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "entityId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setEntityId(@NonNull String)", + "getter": "getEntityId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveStreamingVideoEntity": { + "package": "com.google.android.engage.video.datamodel.LiveStreamingVideoEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "startTimeEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setStartTimeEpochMillis(long)", + "getter": "getStartTimeEpochMillis()" + }, + "endTimeEpochMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setEndTimeEpochMillis(long)", + "getter": "getEndTimeEpochMillis()" + }, + "broadcaster": { + "type": "String", + "requirement": "Required", + "setter": "setBroadcaster(String)", + "getter": "getBroadcaster()" + }, + "viewCount": { + "type": "String", + "requirement": "Optional", + "setter": "setViewCount(String)", + "getter": "getViewCount()" + }, + "broadcasterIcon": { + "type": "Image", + "requirement": "Optional", + "setter": "setBroadcasterIcon(Image)", + "getter": "getBroadcasterIcon()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "genres": { + "type": "List<@NonNull String>", + "requirement": "Optional", + "adder": "addGenre(@NonNull String)", + "getter": "getGenres()", + "adderAll": "addGenres(List)" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "LiveTvChannelEntity": { + "package": "com.google.android.engage.video.datamodel.LiveTvChannelEntity", + "fields": { + "entityId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setEntityId(@NonNull String)", + "getter": "getEntityId()" + }, + "name": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setName(@NonNull String)", + "getter": "getName()" + }, + "description": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setDescription(@NonNull String)", + "getter": "getDescription()" + }, + "logoImage": { + "type": "@NonNull Image", + "requirement": "Optional", + "setter": "setLogoImage(@NonNull Image)", + "getter": "getLogoImage()" + }, + "contentRatings": { + "type": "List<@NonNull RatingSystem>", + "requirement": "Optional", + "adder": "addContentRating(@NonNull RatingSystem)", + "getter": "getContentRatings()", + "adderAll": "addContentRatings(List)" + }, + "platformSpecificPlaybackUris": { + "type": "List<@NonNull PlatformSpecificUri>", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(@NonNull PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "recommendationReason": { + "type": "@NonNull RecommendationReason", + "requirement": "Optional", + "setter": "setRecommendationReason(@NonNull RecommendationReason)", + "getter": "getRecommendationReason()" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + }, + "VideoClipEntity": { + "package": "com.google.android.engage.video.datamodel.VideoClipEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "name": { + "type": "String", + "requirement": "Required", + "setter": "setName(String)", + "getter": "getName()" + }, + "lastEngagementTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastEngagementTimeMillis(long)", + "getter": "getLastEngagementTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "watchNextType": { + "type": "@WatchNextType int", + "requirement": "Optional", + "setter": "setWatchNextType(@WatchNextType int)", + "getter": "getWatchNextType()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastPlayBackPositionTimeMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastPlayBackPositionTimeMillis(long)", + "getter": "getLastPlayBackPositionTimeMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "availabilityTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addAvailabilityTimeWindow(DisplayTimeWindow)", + "getter": "getAvailabilityTimeWindows()" + }, + "allAvailabilityTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllAvailabilityTimeWindows(DisplayTimeWindow)", + "adderAll": "addAllAvailabilityTimeWindows(List)" + }, + "playBackUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setPlayBackUri(Uri)", + "getter": "getPlayBackUri()" + }, + "createdTimeEpochMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setCreatedTimeEpochMillis(long)", + "getter": "getCreatedTimeEpochMillis()" + }, + "durationMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setDurationMillis(long)", + "getter": "getDurationMillis()" + }, + "creator": { + "type": "String", + "requirement": "Required", + "setter": "setCreator(String)", + "getter": "getCreator()" + }, + "creatorImage": { + "type": "Image", + "requirement": "Optional", + "setter": "setCreatorImage(Image)", + "getter": "getCreatorImage()" + }, + "viewCount": { + "type": "String", + "requirement": "Optional", + "setter": "setViewCount(String)", + "getter": "getViewCount()" + }, + "downloadedOnDevice": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setDownloadedOnDevice(boolean)", + "getter": "isDownloadedOnDevice()" + }, + "platformSpecificPlaybackUris": { + "type": "List", + "requirement": "Optional", + "adder": "addPlatformSpecificPlaybackUri(PlatformSpecificUri)", + "getter": "getPlatformSpecificPlaybackUris()", + "adderAll": "addPlatformSpecificPlaybackUris(List)" + }, + "callToActionText": { + "requirement": "Optional", + "setter": "setCallToActionText(@NonNull String)", + "type": "@NonNull String", + "getter": "getCallToActionText()" + }, + "tags": { + "requirement": "Optional", + "adder": "addTag(@NonNull String)", + "type": "List<@NonNull String>", + "adderAll": "addTags(List)", + "getter": "getTags()" + } + } + } + }, + "methods": { + "isServiceAvailable": null, + "publishRecommendationClusters": "PublishRecommendationClustersRequest", + "publishFeaturedCluster": "PublishFeaturedClusterRequest", + "publishContinuationCluster": "PublishContinuationClusterRequest", + "publishSubscription": "PublishSubscriptionRequest", + "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", + "updatePublishStatus": "PublishStatusRequest", + "deleteRecommendationsClusters": "DeleteClustersRequest", + "deleteFeaturedCluster": "DeleteClustersRequest", + "deleteContinuationCluster": "DeleteClustersRequest", + "deleteSubscription": "DeleteClustersRequest", + "deleteUserManagementCluster": "DeleteClustersRequest" + }, + "intents": {} + } \ No newline at end of file diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md new file mode 100644 index 0000000..6223de3 --- /dev/null +++ b/testing/testing-setup/SKILL.md @@ -0,0 +1,198 @@ +--- +name: testing-setup +description: Analyze and create a testing strategy for native Android apps - install + testing libraries, set up test infrastructure, create harnesses for unit tests, + UI tests, screenshot tests, and end-to-end tests. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-15' + keywords: + - android + - testing + - ui tests + - screenshot tests + - coverage +--- + +## 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: + +1. Dependency Injection framework used. Examples: Hilt, Koin, Anvil, vanilla Dagger... +2. Unit (local) testing framework this project uses, Example JUnit4, JUnit5... +3. Mocking framework (if any) used for unit tests, and for Instrumented and UI tests. Examples: Mockito, Mockk... +4. Robolectric. It can be used in 3 ways: + 1. Used in unit tests to have fakes for platform entities + 2. To run behavior UI tests without a device or emulator. For example, used to run Espresso or Compose tests. + 3. To do screenshot testing with Roborazzi +5. Is the app 100% Compose, Views or hybrid? +6. Behavior UI tests: + 1. Compose Tests (`androidx.compose.ui:ui-test-*`) + 2. Espresso Tests for Views. Might use wrappers like Kaspresso. Dependencies: `androidx.test.espresso:espresso-core`, `androidx.test:runner`, `androidx.test:rules`. +7. Screenshot tests can be: + 1. Instrumented (device-based). For example, using Dropshots. + 2. Based on Robolectric, so they run locally. For example, using Roborazzi. + 3. Based on LayoutLib, so they run locally. For example, Paparazzi, or the Compose Preview Screenshot Testing tool. +8. End to end tests (also known as Release Candidate tests) always run on device and use high-level frameworks such as UIAutomator, Appium, or Robotium. +9. Generate a Markdown report with the analysis + +## 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. + +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. + +## 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: + +- 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. +- 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. + +If instrumented screenshot tests are requested, install Dropshots. + +If end-to-end testing is requested, install UI Automator. + +## Step 4: Refactor and create fakes for testing + +### **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: + +- 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. + +### **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: + +- **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. + +## 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. + +## 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. + +## Step 8: Screenshot tests + +Irrespective of the framework used, screenshot tests focus on 2 types of tests: + +- Screen-level screenshot tests, where each screen is tested in 9 different sizes, combining compact, medium and expanded widths (400, 610, 900 dp) and heights (400, 500 and 1000 dp). +- Screen-level variations. Add a mobile (400x500) screenshot of: + - All the alternative themes, if used. + - 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. + +## 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. + +### **Compose UI behavior tests** + +- Use the ComposeTestRule with a `ComponentActivity` to access resources such as strings. +- Always try to match with semantic matchers first. If the matcher is too complicated to write (using more than 3 matchers to find a single element), use `testTag`. +- Always verify state restoration + +### **Views (XML) UI behavior tests** + +Use Espresso to match views and interact with them. + +## Step 10: Navigation tests + +Create a test suite to verify navigation logic. Include: + +- Back handling +- Deeplinks +- Special patterns like "exit through home" with multiple backstacks. + +## 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 + +## 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. + +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. + +### Step 14: Install jacoco + +Install jacoco for local testing code coverage. + +- Add the `jacoco` plugin to each module that contains tests. + +## Final touches + +- 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 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 new file mode 100644 index 0000000..3ae3f34 --- /dev/null +++ b/testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md @@ -0,0 +1,181 @@ +[Video](https://www.youtube.com/watch?v=Y9GWnwi9D0I) + +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. + +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. + +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 { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun myTest() { + // Start the app + composeTestRule.setContent { + MyAppTheme { + MainScreen(uiState = exampleUiState, /*...*/) + } + } + val continueLabel = composeTestRule.activity.getString(R.string.next) + composeTestRule.onNodeWithText(continueLabel).performClick() + } + } + +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`. + + // Creates a semantics property of type Long. + val PickedDateKey = SemanticsPropertyKey("PickedDate") + var SemanticsPropertyReceiver.pickedDate by PickedDateKey + +Now use that property in the `semantics` modifier: + + val datePickerValue by remember { mutableStateOf(0L) } + MyCustomDatePicker( + modifier = Modifier.semantics { pickedDate = datePickerValue } + ) + +From tests, use `SemanticsMatcher.expectValue` to assert the value of the +property: + + composeTestRule + .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21 + .assertExists() + +> [!WARNING] +> **Warning:** You should only use custom Semantics properties when it's hard to match a specific item using the given finders and matchers. Using custom Semantics properties to expose visual properties such as colors, font size or rounded corner radius is not recommended, as it can pollute production code and wrong implementations can lead to bugs that are hard to find. + +### 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. + +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 { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun onRecreation_stateIsRestored() { + val restorationTester = StateRestorationTester(composeTestRule) + + restorationTester.setContent { MainScreen() } + + // TODO: Run actions that modify the state + + // Trigger a recreation + restorationTester.emulateSavedInstanceStateRestore() + + // TODO: Verify that state has been correctly restored. + } + } + +### 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. + +[`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: + +- [`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.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.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. + +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 { + DeviceConfigurationOverride( + DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp)) + ) { + 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)): + + +```kotlin +composeTestRule.setContent { + DeviceConfigurationOverride( + DeviceConfigurationOverride.FontScale(1.5f) then + DeviceConfigurationOverride.FontWeightAdjustment(200) + ) { + Text(text = "text with increased scale and weight") + } +} +``` + +
+ +## Additional Resources + +- **[Test apps on Android](https://developer.android.com/training/testing)**: The main Android testing landing page provides a broader view of testing fundamentals and techniques. +- **[Fundamentals of testing](https://developer.android.com/training/testing/fundamentals):** Learn more about the core concepts behind testing an Android app. +- **[Local tests](https://developer.android.com/training/testing/local-tests):** You can run some tests locally, on your own workstation. +- **[Instrumented tests](https://developer.android.com/training/testing/instrumented-tests):** It is good practice to also run instrumented tests. That is, tests that run directly on-device. +- **[Continuous integration](https://developer.android.com/training/testing/continuous-integration):** Continuous integration lets you integrate your tests into your deployment pipeline. +- **[Test different screen sizes](https://developer.android.com/training/testing/different-screens):** With some many devices available to users, you should test for different screen sizes. +- **[Espresso](https://developer.android.com/training/testing/espresso)**: While intended for View-based UIs, Espresso knowledge can still be helpful for some aspects of Compose testing. \ No newline at end of file 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 new file mode 100644 index 0000000..ed72c2b --- /dev/null +++ b/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md @@ -0,0 +1,227 @@ +> [!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. + +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: + +- Use `@PreviewTest` to create screenshot tests for existing or new composable previews. +- Generate reference images from those composable previews. +- Generate an HTML report that identifies changes to those previews after you make code changes. +- Use `@Preview` parameters, such as `uiMode` or `fontScale`, and multi-previews to help you scale your tests. +- Modularize your tests with the new `screenshotTest` source set. + +![](https://developer.android.com/static/studio/images/compose-screenshot-testing.png) **Figure 1.** Example HTML report. + +## 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: + +- **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: + +- Android Studio Panda 1 Canary 4 or higher. +- Android Gradle Plugin (AGP) version 9.0 or higher. +- Compose Preview Screenshot Testing plugin version [0.0.1-alpha14](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes#alpha14) or higher. +- Kotlin version 2.2.10 or higher. +- 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: + +- Android Gradle Plugin (AGP) version 8.5.0 or higher. +- Compose Preview Screenshot Testing plugin version [0.0.1-alpha14](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes#alpha14) or higher. +- Kotlin version 1.9.20 or higher. We recommend using Kotlin 2.0 or higher so you can use the Compose Compiler Gradle plugin. +- 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). + +> [!NOTE] +> **Note:** If you can't use the Compose Compiler Gradle plugin, you can enable Compose by [declaring a dependency on the Compose Compiler directly](https://developer.android.com/jetpack/androidx/releases/compose-kotlin#kts). Make sure you use `kotlinCompilerExtensionVersion` version 1.5.4 or higher. + +## 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: + +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. + + android { + experimentalProperties["android.experimental.enableScreenshotTest"] = true + } + +3. Add the `com.android.compose.screenshot` plugin, version `0.0.1-alpha14` to + your project. + + 1. Add the plugin to your version catalogs file: + + [versions] + agp = "9.0.0-rc03" + kotlin = "2.1.20" + screenshot = "0.0.1-alpha14" + + [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: + + 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. + + 1. Add them to your version catalogs: + + [libraries] + screenshot-validation-api = { group = "com.android.tools.screenshot", name = "screenshot-validation-api", version.ref = "screenshot"} + androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"} + + 2. Add them to your module-level `build.gradle.kts` file: + + dependencies { + screenshotTestImplementation(libs.screenshot.validation.api) + screenshotTestImplementation(libs.androidx.ui.tooling) + } + +## 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: + +`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. + + package com.example.yourapp + + import androidx.compose.runtime.Composable + import androidx.compose.ui.tooling.preview.Preview + import com.android.tools.screenshot.PreviewTest + import com.example.yourapp.ui.theme.MyApplicationTheme + + @PreviewTest + @Preview(showBackground = true) + @Composable + fun GreetingPreview() { + MyApplicationTheme { + Greeting("Android!") + } + } + +## 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. + +### 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**. + +### With the Gradle tasks + +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`). + +> [!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. + +### In the IDE + +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. + +> [!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: + +- 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`. + +## 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: + + android.compose.screenshot.maxHeapSize=4g + +## Known issues + +- **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). + +## 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 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 new file mode 100644 index 0000000..022bf07 --- /dev/null +++ b/testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md @@ -0,0 +1,447 @@ +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: + +```kotlin +@ActivityScoped +class AnalyticsAdapter @Inject constructor( + private val service: AnalyticsService +) { ... } + +class AnalyticsAdapterTest { + + @Test + fun `Happy path`() { + // You don't need Hilt to create an instance of AnalyticsAdapter. + // You can pass a fake or mock AnalyticsService. + val adapter = AnalyticsAdapter(fakeAnalyticsService) + assertEquals(...) + } +} +``` + +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. + +### Adding testing dependencies + +To use Hilt in your tests, include the `hilt-android-testing` dependency in your +project: + +```kotlin +dependencies { + // For Robolectric tests. + testImplementation("com.google.dagger:hilt-android-testing:2.57.1") + kspTest("com.google.dagger:hilt-android-compiler:2.57.1") + + // For instrumented tests. + androidTestImplementation("com.google.dagger:hilt-android-testing:2.57.1") + kspAndroidTest("com.google.dagger:hilt-android-compiler:2.57.1") + + // Compose UI test rule. + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-test-manifest") + +} +``` + +> [!NOTE] +> **Note:** If you use [Jetpack integrations](https://developer.android.com/training/dependency-injection/hilt-jetpack) (like `hilt-navigation-compose` to obtain a ViewModel through `hiltViewModel()`), you must also add their annotation processors to your test 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. + +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 +class SettingsScreenTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + // Compose UI tests here. +} +``` + +> [!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). + +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. + +#### 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 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: + +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. + +```kotlin +// A custom runner to set up the instrumented application class for tests. +class CustomTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} +``` + +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 { + defaultConfig { + // Replace com.example.android.dagger with your class path. + testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner" + } +} +``` + +##### 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: + +`application = dagger.hilt.android.testing.HiltTestApplication` + +Alternatively, you can configure the application on each test individually by +using Robolectric's `@Config` annotation: + +```kotlin +@HiltAndroidTest +@Config(application = HiltTestApplication::class) +class SettingsScreenTest { + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + // Robolectric tests here. +} +``` + +### Testing features + +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()`. + +See the following example of an instrumented test: + +```kotlin +@HiltAndroidTest +class SettingsScreenTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + @Inject + lateinit var analyticsAdapter: AnalyticsAdapter + + @Before + fun init() { + hiltRule.inject() + } + + @Test + fun settingsScreen_showsTitle() { + composeRule.setContent { + SettingsScreen() + } + composeRule.onNodeWithText("Settings").assertIsDisplayed() + // analyticsRepository is available here. + } +} +``` + +#### 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. + +For example, suppose your production code declares a binding for +`AnalyticsService` as follows: + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +abstract class AnalyticsModule { + + @Singleton + @Binds + abstract fun bindAnalyticsService( + analyticsServiceImpl: AnalyticsServiceImpl + ): AnalyticsService +} +``` + +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 +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [AnalyticsModule::class] +) +abstract class FakeAnalyticsModule { + + @Singleton + @Binds + abstract fun bindAnalyticsService( + fakeAnalyticsService: FakeAnalyticsService + ): AnalyticsService +} +``` + +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. + +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) +@HiltAndroidTest +class SettingsScreenTest { ... } +``` + +Next, you must replace the binding. Create a new module within the test class +that defines the test binding: + +```kotlin +@UninstallModules(AnalyticsModule::class) +@HiltAndroidTest +class SettingsScreenTest { + + @Module + @InstallIn(SingletonComponent::class) + abstract class TestModule { + + @Singleton + @Binds + abstract fun bindAnalyticsService( + fakeAnalyticsService: FakeAnalyticsService + ): AnalyticsService + } + + // ... +} +``` + +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. + +> [!WARNING] +> **Warning:** `@UninstallModules` can only uninstall `@InstallIn` modules, not `@TestInstallIn` modules. Attempting to do so causes a compilation error. + +> [!NOTE] +> **Note:** As Hilt creates new components for tests that use `@UninstallModules`, it can significantly impact unit test build times. Use it when necessary and prefer using `@TestInstallIn` when the bindings need to be replaced in all test classes. + +#### 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. + +In the `AnalyticsService` example, you can replace `AnalyticsService` with a +fake by using `@BindValue`: + +```kotlin +@UninstallModules(AnalyticsModule::class) +@HiltAndroidTest +class SettingsScreenTest { + + @BindValue @JvmField + val analyticsService: AnalyticsService = FakeAnalyticsService() + + ... +} +``` + +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: + +```kotlin +... +class SettingsScreenTest { + ... + + @BindValue @ExampleQualifier @Mock + lateinit var qualifiedVariable: ExampleCustomType + + // Robolectric tests here +} +``` + +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 + +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. + +`@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). + +> [!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`: + +```kotlin +@HiltAndroidTest +class SettingsScreenTest { + + @get:Rule(order = 0) + var hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + @get:Rule(order = 2) + val otherRule = SomeOtherRule() + + // UI tests here. +} +``` + +Alternatively, you can wrap the rules with `RuleChain`, placing +`HiltAndroidRule` as the outer rule. + +```kotlin +@HiltAndroidTest +class SettingsScreenTest { + + @get:Rule + var rule = RuleChain.outerRule(HiltAndroidRule(this)). + around(SettingsScreenTestRule(...)) + + // UI tests here. +} +``` + +### 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. + +More information about `@EarlyEntryPoint` in the +[Hilt documentation](https://dagger.dev/hilt/early-entry-point). + +## Additional resources + +To learn more about testing, see the following additional resources: + +### Documentation + +- [Test your Compose layout](https://developer.android.com/develop/ui/compose/testing) +- [Testing cheatsheet](https://developer.android.com/develop/ui/compose/testing/testing-cheatsheet) + +### Views content + +- [Hilt testing guide (Views)](https://developer.android.com/topic/architecture/views/dependency-injection/hilt-testing-views) \ No newline at end of file From 4328beaf36f00265db107eb316f9add6b8764144 Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Fri, 15 May 2026 19:47:35 +0100 Subject: [PATCH 20/61] Remove Perfetto common files (#42) --- .../profilers/common/perfetto-stdlib.md | 6670 ----------------- 1 file changed, 6670 deletions(-) delete mode 100644 profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md diff --git a/profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md b/profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md deleted file mode 100644 index c1b871b..0000000 --- a/profilers/perfetto-sql/references/android/agents/skills/profilers/common/perfetto-stdlib.md +++ /dev/null @@ -1,6670 +0,0 @@ -*This page documents the PerfettoSQL standard library.* - -## Introduction - -The PerfettoSQL standard library is a repository of tables, views, functions -and macros, contributed by domain experts, which make querying traces easier -Its design is heavily inspired by standard libraries in languages like Python, -C++ and Java. - -Some of the purposes of the standard library include: -1) Acting as a way of sharing and commonly written queries without needing -to copy/paste large amounts of SQL. -2) Raising the abstraction level when exposing data in the trace. Many -modules in the standard library convert low-level trace concepts -e.g. slices, tracks and into concepts developers may be more familar with -e.g. for Android developers: app startups, binder transactions etc. - -Standard library modules can be included as follows - - -- Include all tables/views/functions from the android.startup.startups - -- module in the standard library. - INCLUDE PERFETTO MODULE android.startup.startups; - - -- Use the android_startups table defined in the android.startup.startups - -- module. - SELECT * - FROM android_startups; - -Prelude is a special module is automatically included. It contains key helper -tables, views and functions which are universally useful. - -More information on importing modules is available in the -[syntax documentation](https://developer.android.com/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) -for the `INCLUDE PERFETTO MODULE` statement. - -## Package: prelude - -#### Views/Tables - -
- -**trace_metrics**. Lists all metrics built-into trace processor. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the metric. | - -
- -
- -
- -**track**. Tracks are a fundamental concept in trace processor and represent a "timeline" for events of the same type and with the same context - -
- -VIEW -Tracks are a fundamental concept in trace processor and represent a -"timeline" for events of the same type and with the same context. See -https://perfetto.dev/docs/analysis/trace-processor#tracks for a more -detailed explanation, with examples. - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this track. Identical to \|track_id\|, prefer using \|track_id\| instead. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| name | STRING | Name of the track; can be null for some types of tracks (e.g. thread tracks). | -| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. Alias of `args.arg_set_id`. | -| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | - -
- -
- -
- -**cpu**. Contains information about the CPUs on the device this trace was taken on. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this CPU. Identical to \|ucpu\|, prefer using \|ucpu\| instead. | -| ucpu | UINT | Unique identifier for this CPU. Isn't equal to \|cpu\| for remote machines and is equal to \|cpu\| for the host machine. | -| cpu | UINT | The 0-based CPU core identifier. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| cluster_id | UINT | The cluster id is shared by CPUs in the same cluster. | -| processor | STRING | A string describing this core. | -| machine_id | UINT | Machine identifier, non-null for CPUs on a remote machine. | -| capacity | UINT | Capacity of a CPU of a device, a metric which indicates the relative performance of a CPU on a device For details see: https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt | -| arg_set_id | UINT | Extra key/value pairs associated with this cpu. | - -
- -
- -
- -**cpu_available_frequencies**. Contains the frequency values that the CPUs on the device are capable of running at. - -
- -VIEW -Contains the frequency values that the CPUs on the device are capable of -running at. - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this cpu frequency. | -| cpu | UINT | The CPU for this frequency, meaningful only in single machine traces. For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| freq | UINT | CPU frequency in KHz. | -| ucpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | - -
- -
- -
- -**sched_slice**. This table holds slices with kernel thread scheduling information - -
- -VIEW -This table holds slices with kernel thread scheduling information. These -slices are collected when the Linux "ftrace" data source is used with the -"sched/switch" and "sched/wakeup\*" events enabled. - -The rows in this table will always have a matching row in the \|thread_state\| -table with \|thread_state.state\| = 'Running' - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this scheduling slice. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | -| dur | LONG | The duration of the slice (in nanoseconds). | -| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | UINT | The thread's unique id in the trace. | -| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | -| priority | INT | The kernel priority that the thread ran at. | -| ucpu | UINT | The unique CPU identifier that the slice executed on. | - -
- -
- -
- -**sched** . Shorter alias for table `sched_slice`. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Alias for `sched_slice.id`. | -| type | STRING | Alias for `sched_slice.type`. | -| ts | LONG | Alias for `sched_slice.ts`. | -| dur | LONG | Alias for `sched_slice.dur`. | -| cpu | UINT | Alias for `sched_slice.cpu`. | -| utid | UINT | Alias for `sched_slice.utid`. | -| end_state | STRING | Alias for `sched_slice.end_state`. | -| priority | INT | Alias for `sched_slice.priority`. | -| ucpu | UINT | Alias for `sched_slice.ucpu`. | -| ts_end | UINT | Legacy column, should no longer be used. | - -
- -
- -
- -**thread_state**. This table contains the scheduling state of every thread on the system during the trace. The rows in this table which have \|state\| = 'Running', will have a corresponding row in the \|sched_slice\| table. - -
- -VIEW -This table contains the scheduling state of every thread on the system during -the trace. - -The rows in this table which have \|state\| = 'Running', will have a -corresponding row in the \|sched_slice\| table. - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this thread state. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | -| dur | LONG | The duration of the slice (in nanoseconds). | -| cpu | UINT | The CPU that the thread executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | UINT | The thread's unique id in the trace. | -| state | STRING | The scheduling state of the thread. Can be "Running" or any of the states described in \|sched_slice.end_state\|. | -| io_wait | UINT | Indicates whether this thread was blocked on IO. | -| blocked_function | STRING | The function in the kernel this thread was blocked on. | -| waker_utid | UINT | The unique thread id of the thread which caused a wakeup of this thread. | -| waker_id | UINT | The unique thread state id which caused a wakeup of this thread. | -| irq_context | UINT | Whether the wakeup was from interrupt context or process context. | -| ucpu | UINT | The unique CPU identifier that the thread executed on. | - -
- -
- -
- -**raw**. Contains 'raw' events from the trace for some types of events - -
- -VIEW -Contains 'raw' events from the trace for some types of events. This table -only exists for debugging purposes and should not be relied on in production -usecases (i.e. metrics, standard library etc.) - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this raw event. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp of this event. | -| name | STRING | The name of the event. For ftrace events, this will be the ftrace event name. | -| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | UINT | The thread this event was emitted on. | -| arg_set_id | UINT | The set of key/value pairs associated with this event. | -| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | -| ucpu | UINT | The unique CPU identifier that this event was emitted on. | - -
- -
- -
- -**ftrace_event**. Contains all the ftrace events in the trace - -
- -VIEW -Contains all the ftrace events in the trace. This table exists only for -debugging purposes and should not be relied on in production usecases (i.e. -metrics, standard library etc). Note also that this table might be empty if -raw ftrace parsing has been disabled. - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this ftrace event. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp of this event. | -| name | STRING | The ftrace event name. | -| cpu | UINT | The CPU this event was emitted on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | UINT | The thread this event was emitted on. | -| arg_set_id | UINT | The set of key/value pairs associated with this event. | -| common_flags | UINT | Ftrace event flags for this event. Currently only emitted for sched_waking events. | -| ucpu | UINT | The unique CPU identifier that this event was emitted on. | - -
- -
- -
- -**experimental_sched_upid**. The sched_slice table with the upid column. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this scheduling slice. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | -| dur | LONG | The duration of the slice (in nanoseconds). | -| cpu | UINT | The CPU that the slice executed on (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| utid | UINT | The thread's unique id in the trace. | -| end_state | STRING | A string representing the scheduling state of the kernel thread at the end of the slice. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). | -| priority | INT | The kernel priority that the thread ran at. | -| ucpu | UINT | The unique CPU identifier that the slice executed on. | -| upid | UINT | The process's unique id in the trace. | - -
- -
- -
- -**cpu_track**. Tracks which are associated to a single CPU. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this cpu track. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| name | STRING | Name of the track. | -| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | -| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| ucpu | UINT | The unique CPU identifier that this track is associated with. | - -
- -
- -
- -**cpu_counter_track**. Tracks containing counter-like events associated to a CPU. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Unique identifier for this cpu counter track. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| name | STRING | Name of the track. | -| parent_id | UINT | The track which is the "parent" of this track. Only non-null for tracks created using Perfetto's track_event API. | -| source_arg_set_id | UINT | Args for this track which store information about "source" of this track in the trace. For example: whether this track orginated from atrace, Chrome tracepoints etc. | -| machine_id | UINT | Machine identifier, non-null for tracks on a remote machine. | -| unit | STRING | The units of the counter. This column is rarely filled. | -| description | STRING | The description for this track. For debugging purposes only. | -| cpu | UINT | The CPU that the track is associated with (meaningful only in single machine traces). For multi-machine, join with the `cpu` table on `ucpu` to get the CPU identifier of each machine. | -| ucpu | UINT | The unique CPU identifier that this track is associated with. | - -
- -
- -
- -**counters** . Alias of the `counter` table. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Alias of `counter.id`. | -| type | STRING | Alias of `counter.type`. | -| ts | LONG | Alias of `counter.ts`. | -| track_id | INT | Alias of `counter.track_id`. | -| value | DOUBLE | Alias of `counter.value`. | -| arg_set_id | INT | Alias of `counter.arg_set_id`. | -| name | STRING | Legacy column, should no longer be used. | -| unit | STRING | Legacy column, should no longer be used. | -| description | STRING | Legacy column, should no longer be used. | - -
- -
- -
- -**slice**. Contains slices from userspace which explains what threads were doing during the trace. - -
- -VIEW -Contains slices from userspace which explains what threads were doing -during the trace. - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the slice. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | LONG | The timestamp at the start of the slice (in nanoseconds). | -| dur | LONG | The duration of the slice (in nanoseconds). | -| track_id | INT | The id of the track this slice is located on. | -| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | -| name | STRING | The name of the slice. The name describes what was happening during the slice. | -| depth | INT | The depth of the slice in the current stack of slices. | -| stack_id | LONG | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | -| parent_stack_id | LONG | The stack_id for the parent of this slice. Rarely useful. | -| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | -| arg_set_id | INT | The id of the argument set associated with this slice. | -| thread_ts | LONG | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_dur | LONG | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_instruction_count | LONG | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| thread_instruction_delta | LONG | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| cat | STRING | Alias of `category`. | -| slice_id | LONG | Alias of `id`. | - -
- -
- -
- -**instant**. Contains instant events from userspace which indicates what happened at a single moment in time. - -
- -VIEW -Contains instant events from userspace which indicates what happened at a -single moment in time. - -| Column | Type | Description | -|---|---|---| -| ts | LONG | The timestamp of the instant (in nanoseconds). | -| track_id | INT | The id of the track this instant is located on. | -| name | STRING | The name of the instant. The name describes what happened during the instant. | -| arg_set_id | INT | The id of the argument set associated with this instant. | - -
- -
- -
- -**slices** . Alternative alias of table `slice`. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | UINT | Alias of `slice.id`. | -| type | STRING | Alias of `slice.type`. | -| ts | LONG | Alias of `slice.ts`. | -| dur | LONG | Alias of `slice.dur`. | -| track_id | INT | Alias of `slice.track_id`. | -| category | STRING | Alias of `slice.category`. | -| name | STRING | Alias of `slice.name`. | -| depth | INT | Alias of `slice.depth`. | -| stack_id | LONG | Alias of `slice.stack_id`. | -| parent_stack_id | LONG | Alias of `slice.parent_stack_id`. | -| parent_id | INT | Alias of `slice.parent_id`. | -| arg_set_id | INT | Alias of `slice.arg_set_id`. | -| thread_ts | LONG | Alias of `slice.thread_ts`. | -| thread_dur | LONG | Alias of `slice.thread_dur`. | -| thread_instruction_count | LONG | Alias of `slice.thread_instruction_count`. | -| thread_instruction_delta | LONG | Alias of `slice.thread_instruction_delta`. | -| cat | LONG | Alias of `slice.cat`. | -| slice_id | LONG | Alias of `slice.slice_id`. | - -
- -
- -
- -**thread**. Contains information of threads seen during the trace. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the thread. Prefer using `utid` instead. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| utid | INT | Unique thread id. This is != the OS tid. This is a monotonic number associated to each thread. The OS thread id (tid) cannot be used as primary key because tids and pids are recycled by most kernels. | -| tid | INT | The OS id for this thread. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|utid\| instead. | -| name | STRING | The name of the thread. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | -| start_ts | LONG | The start timestamp of this thread (if known). Is null in most cases unless a thread creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | -| end_ts | LONG | The end timestamp of this thread (if known). Is null in most cases unless a thread destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | -| upid | LONG | The process hosting this thread. | -| is_main_thread | BOOL | Boolean indicating if this thread is the main thread in the process. | -| machine_id | INT | Machine identifier, non-null for threads on a remote machine. | - -
- -
- -
- -**process**. Contains information of processes seen during the trace. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the process. Prefer using `upid` instead. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| upid | LONG | Unique process id. This is != the OS pid. This is a monotonic number associated to each process. The OS process id (pid) cannot be used as primary key because tids and pids are recycled by most kernels. | -| pid | LONG | The OS id for this process. Note: this is *not* unique over the lifetime of the trace so cannot be used as a primary key. Use \|upid\| instead. | -| name | STRING | The name of the process. Can be populated from many sources (e.g. ftrace, /proc scraping, track event etc). | -| start_ts | LONG | The start timestamp of this process (if known). Is null in most cases unless a process creation event is enabled (e.g. task_newtask ftrace event on Linux/Android). | -| end_ts | LONG | The end timestamp of this process (if known). Is null in most cases unless a process destruction event is enabled (e.g. sched_process_free ftrace event on Linux/Android). | -| parent_upid | INT | The upid of the process which caused this process to be spawned. | -| uid | INT | The Unix user id of the process. | -| android_appid | INT | Android appid of this process. | -| cmdline | STRING | /proc/cmdline for this process. | -| arg_set_id | INT | Extra args for this process. | -| machine_id | INT | Machine identifier, non-null for processes on a remote machine. | - -
- -
- -
- -**args**. Arbitrary key-value pairs which allow adding metadata to other, strongly typed tables. Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| will be non-null. - -
- -VIEW -Arbitrary key-value pairs which allow adding metadata to other, strongly -typed tables. -Note: for a given row, only one of \|int_value\|, \|string_value\|, \|real_value\| -will be non-null. - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the arg. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| arg_set_id | INT | The id for a single set of arguments. | -| flat_key | STRING | The "flat key" of the arg: this is the key without any array indexes. | -| key | STRING | The key for the arg. | -| int_value | LONG | The integer value of the arg. | -| string_value | STRING | The string value of the arg. | -| real_value | DOUBLE | The double value of the arg. | -| value_type | STRING | The type of the value of the arg. Will be one of 'int', 'uint', 'string', 'real', 'pointer', 'bool' or 'json'. | -| display_value | STRING | The human-readable formatted value of the arg. | - -
- -
- -
- -**perf_session**. Contains the Linux perf sessions in the trace. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the perf session. Prefer using `perf_session_id` instead. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| perf_session_id | INT | The id of the perf session. | -| cmdline | STRING | Command line used to collect the data. | - -
- -
- -#### Functions - -
- -**slice_is_ancestor** -\> BOOL. Given two slice ids, returns whether the first is an ancestor of the second. - -
- -
- -
- -Returns BOOL: Whether `ancestor_id` slice is an ancestor of `descendant_id`. - -| Argument | Type | Description | -|---|---|---| -| ancestor_id | LONG | Id of the potential ancestor slice. | -| descendant_id | LONG | Id of the potential descendant slice. | - -
- -
- -
- -**trace_start** -\> LONG. Fetch start of the trace. - -
- -
- -
- -Returns LONG: Start of the trace in nanoseconds. - -
- -
- -
- -**trace_end** -\> LONG. Fetch end of the trace. - -
- -
- -
- -Returns LONG: End of the trace in nanoseconds. - -
- -
- -
- -**trace_dur** -\> LONG. Fetch duration of the trace. - -
- -
- -
- -Returns LONG: Duration of the trace in nanoseconds. - -
- -
- -#### Macros - -
- -**cast_int** . Casts \|value\| to INT. - -
- -
- -
- -Returns: Expr, - -| Argument | Type | Description | -|---|---|---| -| value | Expr | Query or subquery that will be cast. | - -
- -
- -
- -**cast_double** . Casts \|value\| to DOUBLE. - -
- -
- -
- -Returns: Expr, - -| Argument | Type | Description | -|---|---|---| -| value | Expr | Query or subquery that will be cast. | - -
- -
- -
- -**cast_string** . Casts \|value\| to STRING. - -
- -
- -
- -Returns: Expr, - -| Argument | Type | Description | -|---|---|---| -| value | Expr | Query or subquery that will be cast. | - -
- -
- -## Package: v8 - -### v8.jit - -#### Views/Tables - -
- -**v8_isolate**. A V8 Isolate instance - -
- -VIEW -A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8 -engine. - -| Column | Type | Description | -|---|---|---| -| v8_isolate_id | UINT | Unique V8 isolate id. | -| upid | UINT | Process the isolate was created in. | -| internal_isolate_id | UINT | Internal id used by the v8 engine. Unique in a process. | -| embedded_blob_code_start_address | LONG | Absolute start address of the embedded code blob. | -| embedded_blob_code_size | LONG | Size in bytes of the embedded code blob. | -| code_range_base_address | LONG | Base address of the code range if the isolate defines one. | -| code_range_size | LONG | Size of a code range if the isolate defines one. | -| shared_code_range | LONG | Whether the code range for this Isolate is shared with others in the same process. There is at max one such shared code range per process. | -| embedded_blob_code_copy_start_address | LONG | Used when short builtin calls are enabled, where embedded builtins are copied into the CodeRange so calls can be nearer. | - -
- -
- -
- -**v8_js_script**. Represents a script that was compiled to generate code - -
- -VIEW -Represents a script that was compiled to generate code. Some V8 code is -generated out of scripts and will reference a V8Script other types of code -will not (e.g. builtins). - -| Column | Type | Description | -|---|---|---| -| v8_js_script_id | UINT | Unique V8 JS script id. | -| v8_isolate_id | UINT | V8 isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | -| internal_script_id | UINT | Script id used by the V8 engine. | -| script_type | STRING | Script type. | -| name | STRING | Script name. | -| source | STRING | Actual contents of the script. | - -
- -
- -
- -**v8_wasm_script**. Represents one WASM script. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| v8_wasm_script_id | UINT | Unique V8 WASM script id. | -| v8_isolate_id | UINT | V8 Isolate this script belongs to (joinable with `v8_isolate.v8_isolate_id`). | -| internal_script_id | UINT | Script id used by the V8 engine. | -| url | STRING | URL of the source. | -| source | STRING | Actual contents of the script. | - -
- -
- -
- -**v8_js_function**. Represents a v8 Javascript function. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| v8_js_function_id | UINT | Unique V8 JS function id. | -| name | STRING | Function name. | -| v8_js_script_id | UINT | Script where the function is defined (joinable with `v8_js_script.v8_js_script_id`). | -| is_toplevel | BOOL | Whether this function represents the top level script. | -| kind | STRING | Function kind (e.g. regular function or constructor). | -| line | UINT | Line in script where function is defined. Starts at 1. | -| col | UINT | Column in script where function is defined. Starts at 1. | - -
- -
- -## Package: wattson - -### wattson.system_state - -#### Views/Tables - -
- -**wattson_system_states**. The final system state for the CPU subsystem, which has all the information needed by Wattson to estimate energy for the CPU subsystem. - -
- -TABLE -The final system state for the CPU subsystem, which has all the information -needed by Wattson to estimate energy for the CPU subsystem. - -| Column | Type | Description | -|---|---|---| -| ts | LONG | Starting timestamp of the current counter where system state is constant. | -| dur | INT | Duration of the current counter where system state is constant. | -| l3_hit_count | INT | Number of L3 hits the current system state. | -| l3_miss_count | INT | Number of L3 misses in the current system state. | -| freq_0 | INT | Frequency of CPU0. | -| idle_0 | INT | Idle state of CPU0. | -| freq_1 | INT | Frequency of CPU1. | -| idle_1 | INT | Idle state of CPU1. | -| freq_2 | INT | Frequency of CPU2. | -| idle_2 | INT | Idle state of CPU2. | -| freq_3 | INT | Frequency of CPU3. | -| idle_3 | INT | Idle state of CPU3. | -| freq_4 | INT | Frequency of CPU4. | -| idle_4 | INT | Idle state of CPU4. | -| freq_5 | INT | Frequency of CPU5. | -| idle_5 | INT | Idle state of CPU5. | -| freq_6 | INT | Frequency of CPU6. | -| idle_6 | INT | Idle state of CPU6. | -| freq_7 | INT | Frequency of CPU7. | -| idle_7 | INT | Idle state of CPU7. | -| suspended | BOOL | Flag indicating if current system state is suspended. | - -
- -
- -## Package: stacks - -### stacks.cpu_profiling - -#### Views/Tables - -
- -**cpu_profiling_samples**. Table containing all the timestamped samples of CPU profiling which occurred during the trace. Currently, this table is backed by the following data sources: \* Linux perf \* macOS instruments \* Chrome CPU profiling \* Legacy V8 CPU profiling \* Profiling data in Gecko traces - -
- -TABLE -Table containing all the timestamped samples of CPU profiling which occurred -during the trace. - -Currently, this table is backed by the following data sources: -\* Linux perf -\* macOS instruments -\* Chrome CPU profiling -\* Legacy V8 CPU profiling -\* Profiling data in Gecko traces - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the sample. | -| ts | INT | The timestamp of the sample. | -| utid | INT | The utid of the thread of the sample, if available. | -| tid | INT | The tid of the sample, if available. | -| thread_name | STRING | The thread name of thread of the sample, if available. | -| ucpu | INT | The ucpu of the sample, if available. | -| cpu | INT | The cpu of the sample, if available. | -| callsite_id | INT | The callsite id of the sample. | - -
- -
- -
- -**cpu_profiling_summary_tree** . Table summarising the callstacks captured during any CPU profiling which occurred during the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. The data sources supported are the same as the `cpu_profiling_samples` table. - -
- -TABLE -Table summarising the callstacks captured during any CPU profiling which -occurred during the trace. - -Specifically, this table returns a tree containing all the callstacks seen -during the trace with `self_count` equal to the number of samples with that -frame as the leaf and `cumulative_count` equal to the number of samples with -the frame anywhere in the tree. - -The data sources supported are the same as the `cpu_profiling_samples` table. - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the callstack; by callstack we mean a unique set of frames up to the root frame. | -| parent_id | INT | The id of the parent callstack for this callstack. NULL if this is root. | -| name | STRING | The function name of the frame for this callstack. | -| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | -| source_file | STRING | The name of the file containing the function. | -| line_number | INT | The line number in the file the function is located at. | -| self_count | INT | The number of samples with this function as the leaf frame. | -| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | - -
- -
- -## Package: pkvm - -### pkvm.hypervisor - -#### Views/Tables - -
- -**pkvm_hypervisor_events**. Events when CPU entered hypervisor. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| slice_id | INT | Id of the corresponding slice in slices table. | -| cpu | INT | CPU that entered hypervisor. | -| ts | INT | Timestamp when CPU entered hypervisor (in nanoseconds). | -| dur | INT | How much time CPU spent in hypervisor (in nanoseconds). | -| reason | STRING | Reason for entering hypervisor (e.g. host_hcall, host_mem_abort), or NULL if unknown. | - -
- -
- -## Package: slices - -### slices.cpu_time - -#### Views/Tables - -
- -**thread_slice_cpu_time**. Time each thread slice spent running on CPU. Requires scheduling data to be available in the trace. - -
- -TABLE -Time each thread slice spent running on CPU. -Requires scheduling data to be available in the trace. - -| Column | Type | Description | -|---|---|---| -| id | INT | Id of a slice. Alias of `slice.id`. | -| name | STRING | Name of the slice. | -| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | -| thread_name | STRING | Name of the thread. | -| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | -| process_name | STRING | Name of the process. | -| cpu_time | INT | Duration of the time the slice was running. | - -
- -
- -
- -**thread_slice_cpu_cycles**. CPU cycles per each slice. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Id of a slice. Alias of `slice.id`. | -| name | STRING | Name of the slice. | -| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | -| thread_name | STRING | Name of the thread. | -| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | -| process_name | STRING | Name of the process. | -| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | - -
- -
- -### slices.with_context - -#### Views/Tables - -
- -**thread_slice**. All thread slices with data about thread, thread track and process. Where possible, use available view functions which filter this view. - -
- -VIEW -All thread slices with data about thread, thread track and process. -Where possible, use available view functions which filter this view. - -| Column | Type | Description | -|---|---|---| -| id | INT | Alias for `slice.id`. | -| type | STRING | Alias for `slice.type`. | -| ts | INT | Alias for `slice.ts`. | -| dur | INT | Alias for `slice.dur`. | -| category | STRING | Alias for `slice.category`. | -| name | STRING | Alias for `slice.name`. | -| track_id | INT | Alias for `slice.track_id`. | -| track_name | STRING | Alias for `thread_track.name`. | -| thread_name | STRING | Alias for `thread.name`. | -| utid | INT | Alias for `thread.utid`. | -| tid | INT | Alias for `thread.tid`. | -| is_main_thread | BOOL | Alias for `thread.is_main_thread`. | -| process_name | STRING | Alias for `process.name`. | -| upid | INT | Alias for `process.upid`. | -| pid | INT | Alias for `process.pid`. | -| depth | INT | Alias for `slice.depth`. | -| parent_id | INT | Alias for `slice.parent_id`. | -| arg_set_id | INT | Alias for `slice.arg_set_id`. | -| thread_ts | INT | Alias for `slice.thread_ts`. | -| thread_dur | INT | Alias for `slice.thread_dur`. | - -
- -
- -
- -**process_slice**. All process slices with data about process track and process. Where possible, use available view functions which filter this view. - -
- -VIEW -All process slices with data about process track and process. -Where possible, use available view functions which filter this view. - -| Column | Type | Description | -|---|---|---| -| id | INT | Alias for `slice.id`. | -| type | STRING | Alias for `slice.type`. | -| ts | INT | Alias for `slice.ts`. | -| dur | INT | Alias for `slice.dur`. | -| category | STRING | Alias for `slice.category`. | -| name | STRING | Alias for `slice.name`. | -| track_id | INT | Alias for `slice.track_id`. | -| track_name | STRING | Alias for `process_track.name`. | -| process_name | STRING | Alias for `process.name`. | -| upid | INT | Alias for `process.upid`. | -| pid | INT | Alias for `process.pid`. | -| depth | INT | Alias for `slice.depth`. | -| parent_id | INT | Alias for `slice.parent_id`. | -| arg_set_id | INT | Alias for `slice.arg_set_id`. | -| thread_ts | INT | Alias for `slice.thread_ts`. | -| thread_dur | INT | Alias for `slice.thread_dur`. | - -
- -
- -## Package: sched - -### sched.runnable - -#### Views/Tables - -
- -**sched_previous_runnable_on_thread**. Previous runnable slice on the same thread. For each "Running" thread state finds: - previous "Runnable" (or runnable preempted) state. - previous uninterrupted "Runnable" state with a valid waker thread. - -
- -TABLE -Previous runnable slice on the same thread. -For each "Running" thread state finds: -- previous "Runnable" (or runnable preempted) state. -- previous uninterrupted "Runnable" state with a valid waker thread. - -| Column | Type | Description | -|---|---|---| -| id | INT | `thread_state.id` id. | -| prev_runnable_id | INT | Previous runnable `thread_state.id`. | -| prev_wakeup_runnable_id | INT | Previous runnable `thread_state.id` with valid waker thread. | - -
- -
- -### sched.states - -#### Functions - -
- -**sched_state_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state to a human-readable string. - -
- -
- -
- -Returns STRING: Humanly readable string representing the scheduling state of the kernel thread. The individual characters in the string mean the following: R (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T (suspended), t (being traced), X (exiting), P (parked), W (waking), I (idle), N (not contributing to the load average), K (wakeable on fatal signals) and Z (zombie, awaiting cleanup). - -| Argument | Type | Description | -|---|---|---| -| short_name | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | - -
- -
- -
- -**sched_state_io_to_human_readable_string** -\> STRING. Translates a single-letter scheduling state and IO wait information to a human-readable string. - -
- -
- -
- -Translates a single-letter scheduling state and IO wait information to -a human-readable string. -Returns STRING: A human readable string with information about the scheduling state and IO wait. - -| Argument | Type | Description | -|---|---|---| -| sched_state | STRING | An individual character string representing the scheduling state of the kernel thread at the end of the slice. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | - -
- -
- -### sched.thread_level_parallelism - -#### Views/Tables - -
- -**sched_runnable_thread_count**. The count of runnable threads over time. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp when the runnable thread count changed to the current value. | -| runnable_thread_count | INT | Number of runnable threads, covering the range from this timestamp to the next row's timestamp. | - -
- -
- -
- -**sched_active_cpu_count**. The count of active CPUs over time. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp when the number of active CPU changed. | -| active_cpu_count | INT | Number of active CPUs, covering the range from this timestamp to the next row's timestamp. | - -
- -
- -### sched.time_in_state - -#### Views/Tables - -
- -**sched_time_in_state_for_thread**. The time a thread spent in each scheduling state during it's lifetime. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| utid | INT | Utid of the thread. | -| total_runtime | INT | Total runtime of thread. | -| state | STRING | One of the scheduling states of kernel thread. | -| time_in_state | INT | Total time spent in the scheduling state. | -| percentage_in_state | INT | Percentage of time thread spent in scheduling state in \[0-100\] range. | - -
- -
- -
- -**sched_percentage_of_time_in_state**. Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] ranges) - -
- -TABLE -Summary of time spent by thread in each scheduling state, in percentage (\[0, 100\] -ranges). Sum of all states might be smaller than 100, as those values -are rounded down. - -| Column | Type | Description | -|---|---|---| -| utid | INT | Utid of the thread. | -| running | INT | Percentage of time thread spent in running ('Running') state in \[0, 100\] range. | -| runnable | INT | Percentage of time thread spent in runnable ('R') state in \[0, 100\] range. | -| runnable_preempted | INT | Percentage of time thread spent in preempted runnable ('R+') state in \[0, 100\] range. | -| sleeping | INT | Percentage of time thread spent in sleeping ('S') state in \[0, 100\] range. | -| uninterruptible_sleep | INT | Percentage of time thread spent in uninterruptible sleep ('D') state in \[0, 100\] range. | -| other | INT | Percentage of time thread spent in other ('T', 't', 'X', 'Z', 'x', 'I', 'K', 'W', 'P', 'N') states in \[0, 100\] range. | - -
- -
- -#### Table Functions - -
- -**sched_time_in_state_for_thread_in_interval** . Time the thread spent each state in a given interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | The start of the interval. | -| dur | INT | The duration of the interval. | -| utid | INT | The utid of the thread. | - -| Column | Type | Description | -|---|---|---| -| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | -| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | -| dur | INT | Total time spent with this state, cpu and blocked function. | - -
- -
- -
- -**sched_time_in_state_and_cpu_for_thread_in_interval** . Time the thread spent each state and cpu in a given interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | The start of the interval. | -| dur | INT | The duration of the interval. | -| utid | INT | The utid of the thread. | - -| Column | Type | Description | -|---|---|---| -| state | INT | Thread state (from the `thread_state` table). Use `sched_state_to_human_readable_string` function to get full name. | -| io_wait | BOOL | A (posssibly NULL) boolean indicating, if the device was in uninterruptible sleep, if it was an IO sleep. | -| cpu | INT | Id of the CPU. | -| blocked_function | INT | Some states can specify the blocked function. Usually NULL. | -| dur | INT | Total time spent with this state, cpu and blocked function. | - -
- -
- -
- -**sched_time_in_state_for_cpu_in_interval** . Time spent by CPU in each scheduling state in a provided interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| cpu | INT | CPU id. | -| ts | INT | Interval start. | -| dur | INT | Interval duration. | - -| Column | Type | Description | -|---|---|---| -| end_state | STRING | End state. From `sched.end_state`. | -| dur | INT | Duration in state. | - -
- -
- -## Package: intervals - -### intervals.overlap - -#### Macros - -
- -**intervals_overlap_count** . Compute the distribution of the overlap of the given intervals over time. Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) counter, with the value corresponding to the number of intervals that overlap the given timestamp and interval until the next timestamp. - -
- -
- -
- -Compute the distribution of the overlap of the given intervals over time. - -Each interval is a (ts, dur) pair and the overlap represented as a (ts, value) -counter, with the value corresponding to the number of intervals that overlap -the given timestamp and interval until the next timestamp. -Returns: TableOrSubquery, The returned table has the schema (ts INT64, value UINT32). \|ts\| is the timestamp when the number of open segments changed. \|value\| is the number of open segments. - -| Argument | Type | Description | -|---|---|---| -| segments | TableOrSubquery | Table or subquery containing interval data. | -| ts_column | ColumnName | Column containing interval starts (usually `ts`). | -| dur_column | ColumnName | Column containing interval durations (usually `dur`). | - -
- -
- -## Package: time - -### time.conversion - -#### Functions - -
- -**time_from_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor - -
- -
- -
- -Returns the provided nanosecond duration, which is the default -representation of time durations in trace processor. Provided for -consistency with other functions. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_from_us** -\> INT. Converts a duration in microseconds to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in microseconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| micros | INT | Time duration in microseconds. | - -
- -
- -
- -**time_from_ms** -\> INT. Converts a duration in millseconds to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in millseconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| millis | INT | Time duration in milliseconds. | - -
- -
- -
- -**time_from_s** -\> INT. Converts a duration in seconds to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in seconds to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| seconds | INT | Time duration in seconds. | - -
- -
- -
- -**time_from_min** -\> INT. Converts a duration in minutes to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in minutes to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| minutes | INT | Time duration in minutes. | - -
- -
- -
- -**time_from_hours** -\> INT. Converts a duration in hours to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in hours to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| hours | INT | Time duration in hours. | - -
- -
- -
- -**time_from_days** -\> INT. Converts a duration in days to nanoseconds, which is the default representation of time durations in trace processor. - -
- -
- -
- -Converts a duration in days to nanoseconds, which is the default -representation of time durations in trace processor. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| days | INT | Time duration in days. | - -
- -
- -
- -**time_to_ns** -\> INT. Returns the provided nanosecond duration, which is the default representation of time durations in trace processor - -
- -
- -
- -Returns the provided nanosecond duration, which is the default -representation of time durations in trace processor. Provided for -consistency with other functions. -Returns INT: Time duration in nanoseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_us** -\> INT. Converts a duration in nanoseconds to microseconds - -
- -
- -
- -Converts a duration in nanoseconds to microseconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in microseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_ms** -\> INT. Converts a duration in nanoseconds to millseconds - -
- -
- -
- -Converts a duration in nanoseconds to millseconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in milliseconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_s** -\> INT. Converts a duration in nanoseconds to seconds - -
- -
- -
- -Converts a duration in nanoseconds to seconds. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in seconds. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_min** -\> INT. Converts a duration in nanoseconds to minutes - -
- -
- -
- -Converts a duration in nanoseconds to minutes. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in minutes. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_hours** -\> INT. Converts a duration in nanoseconds to hours - -
- -
- -
- -Converts a duration in nanoseconds to hours. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in hours. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -
- -**time_to_days** -\> INT. Converts a duration in nanoseconds to days - -
- -
- -
- -Converts a duration in nanoseconds to days. Nanoseconds is the default -representation of time durations in trace processor. -Returns INT: Time duration in days. - -| Argument | Type | Description | -|---|---|---| -| nanos | INT | Time duration in nanoseconds. | - -
- -
- -## Package: linux - -### linux.cpu.frequency - -#### Views/Tables - -
- -**cpu_frequency_counters**. Counter information for each frequency change for each CPU - -
- -TABLE -Counter information for each frequency change for each CPU. Finds each time -region where a CPU frequency is constant. - -| Column | Type | Description | -|---|---|---| -| id | INT | Counter id. | -| track_id | INT | Joinable with 'counter_track.id'. | -| ts | LONG | Starting timestamp of the counter | -| dur | INT | Duration in which counter is constant and frequency doesn't change. | -| freq | INT | Frequency in kHz of the CPU that corresponds to this counter. NULL if not found or undefined. | -| ucpu | INT | Unique CPU id. | -| cpu | INT | CPU that corresponds to this counter. | - -
- -
- -### linux.cpu.idle - -#### Views/Tables - -
- -**cpu_idle_counters**. Counter information for each idle state change for each CPU - -
- -TABLE -Counter information for each idle state change for each CPU. Finds each time -region where a CPU idle state is constant. - -| Column | Type | Description | -|---|---|---| -| id | INT | Counter id. | -| track_id | INT | Joinable with 'counter_track.id'. | -| ts | LONG | Starting timestamp of the counter. | -| dur | INT | Duration in which the counter is contant and idle state doesn't change. | -| idle | INT | Idle state of the CPU that corresponds to this counter. An idle state of -1 is defined to be active state for the CPU, and the larger the integer, the deeper the idle state of the CPU. NULL if not found or undefined. | -| cpu | INT | CPU that corresponds to this counter. | - -
- -
- -### linux.cpu.idle_stats - -#### Views/Tables - -
- -**cpu_idle_stats**. Aggregates cpu idle statistics per core. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| cpu | INT | CPU core number. | -| state | INT | CPU idle state (C-states). | -| count | INT | The count of entering idle state. | -| dur | INT | Total CPU core idle state duration in nanoseconds. | -| avg_dur | INT | Average CPU core idle state duration in nanoseconds. | -| idle_percent | FLOAT | Idle state percentage of non suspend time (C-states + P-states). | - -
- -
- -### linux.cpu.idle_time_in_state - -#### Views/Tables - -
- -**cpu_idle_time_in_state_counters**. Counter information for sysfs cpuidle states. Tracks the percentage of time spent in each state between two timestamps, by dividing the incremental time spent in one state, by time all CPUS spent in any state. - -
- -TABLE -Counter information for sysfs cpuidle states. -Tracks the percentage of time spent in each state between two timestamps, by -dividing the incremental time spent in one state, by time all CPUS spent in -any state. - -| Column | Type | Description | -|---|---|---| -| ts | LONG | Timestamp. | -| state_name | STRING | State name. | -| idle_percentage | DOUBLE | Percentage of time all CPUS spent in this state. | -| total_residency | DOUBLE | Incremental time spent in this state (residency), in microseconds. | -| time_slice | INT | Time all CPUS spent in any state, in microseconds. | - -
- -
- -### linux.cpu.utilization.process - -#### Views/Tables - -
- -**cpu_cycles_per_process**. Aggregated CPU statistics for each process. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| upid | INT | Unique process id | -| millicycles | INT | Sum of CPU millicycles | -| megacycles | INT | Sum of CPU megacycles | -| runtime | INT | Total runtime duration | -| min_freq | INT | Minimum CPU frequency in kHz | -| max_freq | INT | Maximum CPU frequency in kHz | -| avg_freq | INT | Average CPU frequency in kHz | - -
- -
- -#### Table Functions - -
- -**cpu_process_utilization_per_period** . Returns a table of process utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| - -
- -
- -
- -Returns a table of process utilization per given period. -Utilization is calculated as sum of average utilization of each CPU in each -period, which is defined as a multiply of \|interval\|. For this reason -first and last period might have lower then real utilization. -Argument \| Type \| Description ---- \| --- \| --- -interval \| INT \| Length of the period on which utilization should be averaged. -upid \| INT \| Upid of the process. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_process_utilization_per_second** . Returns a table of process utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| - -
- -
- -
- -Returns a table of process utilization per second. -Utilization is calculated as sum of average utilization of each CPU in each -period, which is defined as a multiply of \|interval\|. For this reason -first and last period might have lower then real utilization. -Argument \| Type \| Description ---- \| --- \| --- -upid \| INT \| Upid of the process. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_cycles_per_process_in_interval** . Aggregated CPU statistics for each process in a provided interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | Start of the interval. | -| dur | INT | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| upid | INT | Unique process id. Joinable with `process.id`. | -| millicycles | INT | Sum of CPU millicycles | -| megacycles | INT | Sum of CPU megacycles | -| runtime | INT | Total runtime duration | -| min_freq | INT | Minimum CPU frequency in kHz | -| max_freq | INT | Maximum CPU frequency in kHz | -| avg_freq | INT | Average CPU frequency in kHz | - -
- -
- -### linux.cpu.utilization.slice - -#### Views/Tables - -
- -**cpu_cycles_per_thread_slice**. CPU cycles per each slice. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | Id of a slice. Alias of `slice.id`. | -| name | STRING | Name of the slice. | -| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | -| thread_name | STRING | Name of the thread. | -| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | -| process_name | STRING | Name of the process. | -| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | - -
- -
- -#### Table Functions - -
- -**cpu_cycles_per_thread_slice_in_interval** . CPU cycles per each slice in interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | Start of the interval. | -| dur | INT | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| id | INT | Id of a slice. Alias of `slice.id`. | -| name | STRING | Name of the slice. | -| utid | INT | Id of the thread the slice is running on. Alias of `thread.id`. | -| thread_name | STRING | Name of the thread. | -| upid | INT | Id of the process the slice is running on. Alias of `process.id`. | -| process_name | STRING | Name of the process. | -| millicycles | INT | Sum of CPU millicycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | -| megacycles | INT | Sum of CPU megacycles. Null if frequency couldn't be fetched for any period during the runtime of the slice. | - -
- -
- -### linux.cpu.utilization.system - -#### Views/Tables - -
- -**cpu_utilization_per_second**. Table with system utilization per second. Utilization is calculated by sum of average utilization of each CPU every second - -
- -TABLE -Table with system utilization per second. -Utilization is calculated by sum of average utilization of each CPU every -second. For this reason first and last second might have lower then real -utilization. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_cycles**. Aggregated CPU statistics for whole trace - -
- -TABLE -Aggregated CPU statistics for whole trace. Results in only one row. - -| Column | Type | Description | -|---|---|---| -| millicycles | INT | Sum of CPU millicycles. | -| megacycles | INT | Sum of CPU megacycles. | -| runtime | INT | Total runtime of all threads running on all CPUs. | -| min_freq | INT | Minimum CPU frequency in kHz. | -| max_freq | INT | Maximum CPU frequency in kHz. | -| avg_freq | INT | Average CPU frequency in kHz. | - -
- -
- -
- -**cpu_cycles_per_cpu**. Aggregated CPU statistics for each CPU. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | -| cpu | INT | The number of the CPU. Might not be the same as ucpu in multi machine cases. | -| millicycles | INT | Sum of CPU millicycles. | -| megacycles | INT | Sum of CPU megacycles. | -| runtime | INT | Total runtime of all threads running on CPU. | -| min_freq | INT | Minimum CPU frequency in kHz. | -| max_freq | INT | Maximum CPU frequency in kHz. | -| avg_freq | INT | Average CPU frequency in kHz. | - -
- -
- -#### Table Functions - -
- -**cpu_utilization_per_period** . Returns a table of system utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| - -
- -
- -
- -Returns a table of system utilization per given period. -Utilization is calculated as sum of average utilization of each CPU in each -period, which is defined as a multiply of \|interval\|. For this reason -first and last period might have lower then real utilization. -Argument \| Type \| Description ---- \| --- \| --- -interval \| INT \| Length of the period on which utilization should be averaged. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_cycles_in_interval** . Aggregated CPU statistics in a provided interval - -
- -
- -
- -Aggregated CPU statistics in a provided interval. Results in one row. -Argument \| Type \| Description ---- \| --- \| --- -ts \| INT \| Start of the interval. -dur \| INT \| Duration of the interval. - -| Column | Type | Description | -|---|---|---| -| millicycles | INT | Sum of CPU millicycles. | -| megacycles | INT | Sum of CPU megacycles. | -| runtime | INT | Total runtime of all threads running on all CPUs. | -| min_freq | INT | Minimum CPU frequency in kHz. | -| max_freq | INT | Maximum CPU frequency in kHz. | -| avg_freq | INT | Average CPU frequency in kHz. | - -
- -
- -
- -**cpu_cycles_per_cpu_in_interval** . Aggregated CPU statistics for each CPU in a provided interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | Start of the interval. | -| dur | INT | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| ucpu | INT | Unique CPU id. Joinable with `cpu.id`. | -| cpu | INT | CPU number. | -| millicycles | INT | Sum of CPU millicycles. | -| megacycles | INT | Sum of CPU megacycles. | -| runtime | INT | Total runtime of all threads running on CPU. | -| min_freq | INT | Minimum CPU frequency in kHz. | -| max_freq | INT | Maximum CPU frequency in kHz. | -| avg_freq | INT | Average CPU frequency in kHz. | - -
- -
- -### linux.cpu.utilization.thread - -#### Views/Tables - -
- -**cpu_cycles_per_thread**. Aggregated CPU statistics for each thread. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| utid | INT | Unique thread id | -| millicycles | INT | Sum of CPU millicycles | -| megacycles | INT | Sum of CPU megacycles | -| runtime | INT | Total runtime duration | -| min_freq | INT | Minimum CPU frequency in kHz | -| max_freq | INT | Maximum CPU frequency in kHz | -| avg_freq | INT | Average CPU frequency in kHz | - -
- -
- -#### Table Functions - -
- -**cpu_thread_utilization_per_period** . Returns a table of thread utilization per given period. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| - -
- -
- -
- -Returns a table of thread utilization per given period. -Utilization is calculated as sum of average utilization of each CPU in each -period, which is defined as a multiply of \|interval\|. For this reason -first and last period might have lower then real utilization. -Argument \| Type \| Description ---- \| --- \| --- -interval \| INT \| Length of the period on which utilization should be averaged. -utid \| INT \| Utid of the thread. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_thread_utilization_per_second** . Returns a table of thread utilization per second. Utilization is calculated as sum of average utilization of each CPU in each period, which is defined as a multiply of \|interval\| - -
- -
- -
- -Returns a table of thread utilization per second. -Utilization is calculated as sum of average utilization of each CPU in each -period, which is defined as a multiply of \|interval\|. For this reason -first and last period might have lower then real utilization. -Argument \| Type \| Description ---- \| --- \| --- -utid \| INT \| Utid of the thread. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp of start of a second. | -| utilization | DOUBLE | Sum of average utilization over period. Note: as the data is normalized, the values will be in the \[0, 1\] range. | -| unnormalized_utilization | DOUBLE | Sum of average utilization over all CPUs over period. Note: as the data is unnormalized, the values will be in the \[0, cpu_count\] range. | - -
- -
- -
- -**cpu_cycles_per_thread_in_interval** . Aggregated CPU statistics for each thread in a provided interval. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| ts | INT | Start of the interval. | -| dur | INT | Duration of the interval. | - -| Column | Type | Description | -|---|---|---| -| utid | INT | Unique thread id. Joinable with `thread.id`. | -| millicycles | INT | Sum of CPU millicycles | -| megacycles | INT | Sum of CPU megacycles | -| runtime | INT | Total runtime duration | -| min_freq | INT | Minimum CPU frequency in kHz | -| max_freq | INT | Maximum CPU frequency in kHz | -| avg_freq | INT | Average CPU frequency in kHz | - -
- -
- -### linux.devfreq - -#### Views/Tables - -
- -**linux_devfreq_dsu_counter**. ARM DSU device frequency counters - -
- -TABLE -ARM DSU device frequency counters. This table will only be populated on -traces collected with "devfreq/devfreq_frequency" ftrace event enabled, -and from ARM devices with the DSU (DynamIQ Shared Unit) hardware. - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique identifier for this counter. | -| ts | LONG | Starting timestamp of the counter. | -| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | -| dsu_freq | INT | Frequency in kHz of the device that corresponds to the counter. | - -
- -
- -#### Table Functions - -
- -**linux_get_devfreq_counters** . Gets devfreq frequency counter based on device queried - -
- -
- -
- -Gets devfreq frequency counter based on device queried. These counters will -only be available if the "devfreq/devfreq_frequency" ftrace event is enabled. -Argument \| Type \| Description ---- \| --- \| --- -device_name \| STRING \| Devfreq name to query for. - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique identifier for this counter. | -| ts | LONG | Starting timestamp of the counter. | -| dur | INT | Duration in which counter is constant and frequency doesn't chamge. | -| freq | INT | Frequency in kHz of the device that corresponds to the counter. | - -
- -
- -### linux.memory.high_watermark - -#### Views/Tables - -
- -**memory_rss_high_watermark_per_process**. For each process fetches the memory high watermark until or during timestamp. - -
- -VIEW -For each process fetches the memory high watermark until or during -timestamp. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp | -| dur | INT | Duration | -| upid | INT | Upid of the process | -| pid | INT | Pid of the process | -| process_name | STRING | Name of the process | -| rss_high_watermark | INT | Maximum `rss` value until now | - -
- -
- -### linux.memory.process - -#### Views/Tables - -
- -**memory_rss_and_swap_per_process**. Memory metrics timeline for each process. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp | -| dur | INT | Duration | -| upid | INT | Upid of the process | -| pid | INT | Pid of the process | -| process_name | STRING | Name of the process | -| anon_rss | INT | Anon RSS counter value | -| file_rss | INT | File RSS counter value | -| shmem_rss | INT | Shared memory RSS counter value | -| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | -| swap | INT | Swap counter value | -| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | -| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | - -
- -
- -### linux.perf.samples - -#### Views/Tables - -
- -**linux_perf_samples_summary_tree** . Table summarising the callstacks captured during all perf samples in the trace. Specifically, this table returns a tree containing all the callstacks seen during the trace with `self_count` equal to the number of samples with that frame as the leaf and `cumulative_count` equal to the number of samples with the frame anywhere in the tree. - -
- -TABLE -Table summarising the callstacks captured during all -perf samples in the trace. - -Specifically, this table returns a tree containing all -the callstacks seen during the trace with `self_count` -equal to the number of samples with that frame as the -leaf and `cumulative_count` equal to the number of -samples with the frame anywhere in the tree. - -| Column | Type | Description | -|---|---|---| -| id | INT | The id of the callstack. A callstack in this context is a unique set of frames up to the root. | -| parent_id | INT | The id of the parent callstack for this callstack. | -| name | STRING | The function name of the frame for this callstack. | -| mapping_name | STRING | The name of the mapping containing the frame. This can be a native binary, library, JAR or APK. | -| source_file | STRING | The name of the file containing the function. | -| line_number | INT | The line number in the file the function is located at. | -| self_count | INT | The number of samples with this function as the leaf frame. | -| cumulative_count | INT | The number of samples with this function appearing anywhere on the callstack. | - -
- -
- -### linux.perf.spe - -#### Views/Tables - -
- -**linux_perf_spe_record**. Contains ARM Statistical Profiling Extension records - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| ts | LONG | Timestap when the operation was sampled | -| utid | INT | Thread the operation executed in | -| exception_level | STRING | Exception level the instruction was executed in | -| instruction_frame_id | INT | Instruction virtual address | -| operation | STRING | Type of operation sampled | -| data_virtual_address | LONG | The virtual address accessed by the operation (0 if no memory access was performed) | -| data_physical_address | LONG | The physical address accessed by the operation (0 if no memory access was performed) | -| total_latency | INT | Cycle count from the operation being dispatched for issue to the operation being complete. | -| issue_latency | INT | Cycle count from the operation being dispatched for issue to the operation being issued for execution. | -| translation_latency | INT | Cycle count from a virtual address being passed to the MMU for translation to the result of the translation being available. | -| data_source | STRING | Where the data returned for a load operation was sourced | -| exception_gen | BOOL | Operation generated an exception | -| retired | BOOL | Operation architecturally retired | -| l1d_access | BOOL | Operation caused a level 1 data cache access | -| l1d_refill | BOOL | Operation caused a level 1 data cache refill | -| tlb_access | BOOL | Operation caused a TLB access | -| tlb_refill | BOOL | Operation caused a TLB refill involving at least one translation table walk | -| not_taken | BOOL | Conditional instruction failed its condition code check | -| mispred | BOOL | Whether a branch caused a correction to the predicted program flow | -| llc_access | BOOL | Operation caused a last level data or unified cache access | -| llc_refill | BOOL | Whether the operation could not be completed by the last level data cache (or any above) | -| remote_access | BOOL | Operation caused an access to another socket in a multi-socket system | -| alignment | BOOL | Operation that incurred additional latency due to the alignment of the address and the size of the data being accessed | -| tme_transaction | BOOL | Whether the operation executed in transactional state | -| sve_partial_pred | BOOL | SVE or SME operation with at least one false element in the governing predicate(s) | -| sve_empty_pred | BOOL | SVE or SME operation with no true element in the governing predicate(s) | -| l2d_access | BOOL | Whether a load operation caused a cache access to at least the level 2 data or unified cache | -| l2d_hit | BOOL | Whether a load operation accessed and missed the level 2 data or unified cache. Not set for accesses that are satisfied from refilling data of a previous miss | -| cache_data_modified | BOOL | Whether a load operation accessed modified data in a cache | -| recenty_fetched | BOOL | Wheter a load operation hit a recently fetched line in a cache | -| data_snooped | BOOL | Whether a load operation snooped data from a cache outside the cache hierarchy of this core | - -
- -
- -### linux.threads - -#### Views/Tables - -
- -**linux_kernel_threads**. All kernel threads of the trace - -
- -TABLE -All kernel threads of the trace. As kernel threads are processes, provides -also process data. - -| Column | Type | Description | -|---|---|---| -| upid | INT | Upid of kernel thread. Alias of \|process.upid\|. | -| utid | INT | Utid of kernel thread. Alias of \|thread.utid\|. | -| pid | INT | Pid of kernel thread. Alias of \|process.pid\|. | -| tid | INT | Tid of kernel thread. Alias of \|process.pid\|. | -| process_name | STRING | Name of kernel process. Alias of \|process.name\|. | -| thread_name | STRING | Name of kernel thread. Alias of \|thread.name\|. | -| machine_id | INT | Machine id of kernel thread. If NULL then it's a single machine trace. Alias of \|process.machine_id\|. | - -
- -
- -## Package: android - -### android.anrs - -#### Views/Tables - -
- -**android_anrs**. List of all ANRs that occurred in the trace (one row per ANR). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| process_name | STRING | Name of the process that triggered the ANR. | -| pid | INT | PID of the process that triggered the ANR. | -| upid | INT | UPID of the process that triggered the ANR. | -| error_id | STRING | UUID of the ANR (generated on the platform). | -| ts | INT | Timestamp of the ANR. | -| subject | STRING | Subject line of the ANR. | - -
- -
- -### android.app_process_starts - -#### Views/Tables - -
- -**android_app_process_starts**. All app cold starts with information about their cold start reason: broadcast, service, activity or provider. - -
- -TABLE -All app cold starts with information about their cold start reason: -broadcast, service, activity or provider. - -| Column | Type | Description | -|---|---|---| -| start_id | INT | Slice id of the bindApplication slice in the app. Uniquely identifies a process start. | -| id | INT | Slice id of intent received in the app. | -| track_id | INT | Track id of the intent received in the app. | -| process_name | STRING | Name of the process receiving the intent. | -| pid | INT | Pid of the process receiving the intent. | -| upid | INT | Upid of the process receiving the intent. | -| intent | STRING | Intent action or component responsible for the cold start. | -| reason | STRING | Process start reason: activity, broadcast, service or provider. | -| proc_start_ts | INT | Timestamp the process start was dispatched from system_server. | -| proc_start_dur | INT | Duration to dispatch the process start from system_server. | -| bind_app_ts | INT | Timestamp the bindApplication started in the app. | -| bind_app_dur | INT | Duration to complete bindApplication in the app. | -| intent_ts | INT | Timestamp the Intent was received in the app. | -| intent_dur | INT | Duration to handle intent in the app. | -| total_dur | INT | Total duration from proc_start dispatched to intent completed. | - -
- -
- -### android.auto.multiuser - -#### Views/Tables - -
- -**android_auto_multiuser_timing**. Time elapsed between the latest user start and the specific end event like package startup(ex carlauncher) or previous user stop. - -
- -TABLE -Time elapsed between the latest user start -and the specific end event -like package startup(ex carlauncher) or previous user stop. - -| Column | Type | Description | -|---|---|---| -| event_start_user_id | STRING | Id of the started android user | -| event_start_time | INT | Start event time | -| event_end_time | INT | End event time | -| event_end_name | STRING | End event name | -| event_start_name | STRING | Start event name | -| duration | LONG | User switch duration from start event to end event | - -
- -
- -
- -**android_auto_multiuser_timing_with_previous_user_resource_usage** . This table extends `android_auto_multiuser_timing` table with previous user resource usage. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| event_start_user_id | STRING | Start user id | -| event_start_time | INT | Start event time | -| event_end_time | INT | End event time | -| event_end_name | STRING | End event name | -| event_start_name | STRING | Start event name | -| duration | LONG | User switch duration from start event to end event | -| user_id | INT | User id | -| total_cpu_time | LONG | Total CPU time for a user | -| total_memory_usage_kb | LONG | Total memory user for a user | - -
- -
- -### android.battery - -#### Views/Tables - -
- -**android_battery_charge**. Battery charge at timestamp. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp. | -| current_avg_ua | DOUBLE | Current average micro ampers. | -| capacity_percent | DOUBLE | Current capacity percentage. | -| charge_uah | DOUBLE | Current charge in micro ampers. | -| current_ua | DOUBLE | Current micro ampers. | - -
- -
- -### android.battery_stats - -#### Views/Tables - -
- -**android_battery_stats_state**. View of human readable battery stats counter-based states - -
- -VIEW -View of human readable battery stats counter-based states. These are recorded -by BatteryStats as a bitmap where each 'category' has a unique value at any -given time. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp in nanoseconds. | -| dur | INT | The duration the state was active, may be negative for incomplete slices. | -| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | -| track_name | STRING | The name of the counter track. | -| value | INT | The counter value as a number. | -| value_name | STRING | The counter value as a human-readable string. | - -
- -
- -
- -**android_battery_stats_event_slices**. View of slices derived from battery_stats events - -
- -VIEW -View of slices derived from battery_stats events. Battery stats records all -events as instants, however some may indicate whether something started or -stopped with a '+' or '-' prefix. Events such as jobs, top apps, foreground -apps or long wakes include these details and allow drawing slices between -instant events found in a trace. - -For example, we may see an event like the following on 'battery_stats.top': - - -top=10215:"com.google.android.apps.nexuslauncher" - -This view will find the associated start ('+top') with the matching suffix -(everything after the '=') to construct a slice. It computes the timestamp -and duration from the events and extract the details as follows: - - track_name='battery_stats.top' - str_value='com.google.android.apps.nexuslauncher' - int_value=10215 - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp in nanoseconds. | -| dur | INT | The duration the state was active, may be negative for incomplete slices. | -| safe_dur | INT | The same as `dur`, but extends to trace end for incomplete slices. | -| track_name | STRING | The name of the counter track. | -| str_value | STRING | String value. | -| int_value | INT | Int value. | - -
- -
- -#### Functions - -
- -**android_battery_stats_counter_to_string** -\> STRING. Converts a battery_stats counter value to human readable string. - -
- -
- -
- -Returns STRING: The human-readable name for the counter value. - -| Argument | Type | Description | -|---|---|---| -| track | STRING | The counter track name (e.g. 'battery_stats.audio'). | -| value | FLOAT | The counter value. | - -
- -
- -### android.binder - -#### Views/Tables - -
- -**android_binder_metrics_by_process**. Count Binder transactions per process. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| process_name | STRING | Name of the process that started the binder transaction. | -| pid | INT | PID of the process that started the binder transaction. | -| slice_name | STRING | Name of the slice with binder transaction. | -| event_count | INT | Number of binder transactions in process in slice. | - -
- -
- -
- -**android_sync_binder_thread_state_by_txn**. Aggregated thread_states on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the thread state durations. The \|thread_state_type\| column represents whether a given 'aggregated thread_state' row is on the client or server side - -
- -VIEW -Aggregated thread_states on the client and server side per binder txn -This builds on the data from \|_sync_binder_metrics_by_txn\| and -for each end (client and server) of the transaction, it returns -the aggregated sum of all the thread state durations. -The \|thread_state_type\| column represents whether a given 'aggregated thread_state' -row is on the client or server side. 'binder_txn' is client side and 'binder_reply' -is server side. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | INT | slice id of the binder txn | -| client_ts | INT | Client timestamp | -| client_tid | INT | Client tid | -| binder_reply_id | INT | slice id of the binder reply | -| server_ts | INT | Server timestamp | -| server_tid | INT | Server tid | -| thread_state_type | STRING | whether thread state is on the txn or reply side | -| thread_state | STRING | a thread_state that occurred in the txn | -| thread_state_dur | INT | aggregated dur of the \|thread_state\| in the txn | -| thread_state_count | INT | aggregated count of the \|thread_state\| in the txn | - -
- -
- -
- -**android_sync_binder_blocked_functions_by_txn**. Aggregated blocked_functions on the client and server side per binder txn This builds on the data from \|_sync_binder_metrics_by_txn\| and for each end (client and server) of the transaction, it returns the aggregated sum of all the kernel blocked function durations. The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' row is on the client or server side - -
- -VIEW -Aggregated blocked_functions on the client and server side per binder txn -This builds on the data from \|_sync_binder_metrics_by_txn\| and -for each end (client and server) of the transaction, it returns -the aggregated sum of all the kernel blocked function durations. -The \|thread_state_type\| column represents whether a given 'aggregated blocked_function' -row is on the client or server side. 'binder_txn' is client side and 'binder_reply' -is server side. - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | INT | slice id of the binder txn | -| client_ts | INT | Client ts | -| client_tid | INT | Client tid | -| binder_reply_id | INT | slice id of the binder reply | -| server_ts | INT | Server ts | -| server_tid | INT | Server tid | -| thread_state_type | STRING | whether thread state is on the txn or reply side | -| blocked_function | STRING | blocked kernel function in a thread state | -| blocked_function_dur | INT | aggregated dur of the \|blocked_function\| in the txn | -| blocked_function_count | INT | aggregated count of the \|blocked_function\| in the txn | - -
- -
- -
- -**android_binder_txns**. Breakdown binder transactions per txn. It returns data about the client and server ends of every binder transaction async. - -
- -TABLE -Breakdown binder transactions per txn. -It returns data about the client and server ends of every binder transaction async. - -| Column | Type | Description | -|---|---|---| -| aidl_name | STRING | name of the binder interface if existing. | -| aidl_ts | INT | Timestamp the binder interface name was emitted. Proxy to 'ts' and 'dur' for async txns. | -| aidl_dur | INT | Duration of the binder interface name. Proxy to 'ts' and 'dur' for async txns. | -| binder_txn_id | INT | slice id of the binder txn. | -| client_process | STRING | name of the client process. | -| client_thread | STRING | name of the client thread. | -| client_upid | INT | Upid of the client process. | -| client_utid | INT | Utid of the client thread. | -| client_tid | INT | Tid of the client thread. | -| client_pid | INT | Pid of the client thread. | -| is_main_thread | BOOL | Whether the txn was initiated from the main thread of the client process. | -| client_ts | INT | timestamp of the client txn. | -| client_dur | INT | wall clock dur of the client txn. | -| binder_reply_id | INT | slice id of the binder reply. | -| server_process | STRING | name of the server process. | -| server_thread | STRING | name of the server thread. | -| server_upid | INT | Upid of the server process. | -| server_utid | INT | Utid of the server thread. | -| server_tid | INT | Tid of the server thread. | -| server_pid | INT | Pid of the server thread. | -| server_ts | INT | timestamp of the server txn. | -| server_dur | INT | wall clock dur of the server txn. | -| client_oom_score | INT | oom score of the client process at the start of the txn. | -| server_oom_score | INT | oom score of the server process at the start of the reply. | -| is_sync | BOOL | whether the txn is synchronous or async (oneway). | -| client_monotonic_dur | INT | monotonic clock dur of the client txn. | -| server_monotonic_dur | INT | monotonic clock dur of the server txn. | -| client_package_version_code | INT | Client package version_code. | -| server_package_version_code | INT | Server package version_code. | -| is_client_package_debuggable | INT | Whether client package is debuggable. | -| is_server_package_debuggable | INT | Whether server package is debuggable. | - -
- -
- -#### Table Functions - -
- -**android_binder_outgoing_graph** . Returns a DAG of all outgoing binder txns from a process. The roots of the graph are the threads making the txns and the graph flows from: thread -\> server_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. - -
- -
- -
- -Returns a DAG of all outgoing binder txns from a process. -The roots of the graph are the threads making the txns and the graph flows from: -thread -\> server_process -\> AIDL interface -\> AIDL method. -The weights of each node represent the wall execution time in the server_process. -Argument \| Type \| Description ---- \| --- \| --- -upid \| INT \| Upid of process to generate an outgoing graph for. - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of outgoing binder txns. | - -
- -
- -
- -**android_binder_incoming_graph** . Returns a DAG of all incoming binder txns from a process. The roots of the graph are the clients making the txns and the graph flows from: client_process -\> AIDL interface -\> AIDL method. The weights of each node represent the wall execution time in the server_process. - -
- -
- -
- -Returns a DAG of all incoming binder txns from a process. -The roots of the graph are the clients making the txns and the graph flows from: -client_process -\> AIDL interface -\> AIDL method. -The weights of each node represent the wall execution time in the server_process. -Argument \| Type \| Description ---- \| --- \| --- -upid \| INT \| Upid of process to generate an incoming graph for. - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of incoming binder txns. | - -
- -
- -
- -**android_binder_graph** . Returns a graph of all binder txns in a trace. The nodes are client_process and server_process. The weights of each node represent the wall execution time in the server_process. - -
- -
- -
- -Returns a graph of all binder txns in a trace. -The nodes are client_process and server_process. -The weights of each node represent the wall execution time in the server_process. -Argument \| Type \| Description ---- \| --- \| --- -min_client_oom_score \| INT \| Matches txns from client_processes greater than or equal to the OOM score. -max_client_oom_score \| INT \| Matches txns from client_processes less than or equal to the OOM score. -min_server_oom_score \| INT \| Matches txns to server_processes greater than or equal to the OOM score. -max_server_oom_score \| INT \| Matches txns to server_processes less than or equal to the OOM score. - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of binder txns. | - -
- -
- -### android.binder_breakdown - -#### Views/Tables - -
- -**android_binder_server_breakdown**. Server side binder breakdowns per transactions per txn. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | -| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | -| ts | INT | Timestamp of an exclusive interval during the binder reply with a single reason. | -| dur | INT | Duration of an exclusive interval during the binder reply with a single reason. | -| reason | STRING | Cause of delay during an exclusive interval of the binder reply. | - -
- -
- -
- -**android_binder_client_breakdown**. Client side binder breakdowns per transactions per txn. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| binder_txn_id | INT | Client side id of the binder txn. Alias of `slice.id`. | -| binder_reply_id | INT | Server side id of the binder txn. Alias of `slice.id`. | -| ts | INT | Timestamp of an exclusive interval during the binder txn with a single latency reason. | -| dur | INT | Duration of an exclusive interval during the binder txn with a single latency reason. | -| reason | STRING | Cause of delay during an exclusive interval of the binder txn. | - -
- -
- -### android.cpu.cluster_type - -#### Views/Tables - -
- -**android_cpu_cluster_mapping**. Stores the mapping of a cpu to its cluster type - e.g - -
- -TABLE -Stores the mapping of a cpu to its cluster type - e.g. little, medium, big. -This cluster type is determined by initially using cpu_capacity from sysfs -and grouping clusters with identical capacities, ordered by size. -In the case that capacities are not present, max frequency is used instead. -If nothing is avaiable, NULL is returned. - -| Column | Type | Description | -|---|---|---| -| ucpu | INT | Alias of `cpu.ucpu`. | -| cpu | INT | Alias of `cpu.cpu`. | -| cluster_type | STRING | The cluster type of the CPU. | - -
- -
- -### android.device - -#### Views/Tables - -
- -**android_device_name**. Extract name of the device based on metadata from the trace. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| name | STRING | Device name. | - -
- -
- -### android.dvfs - -#### Views/Tables - -
- -**android_dvfs_counters**. Dvfs counter with duration. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name. | -| ts | INT | Timestamp when counter value changed. | -| value | DOUBLE | Counter value. | -| dur | INT | Counter duration. | - -
- -
- -
- -**android_dvfs_counter_stats**. Aggregates dvfs counter slice for statistic. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name on which all the other values are aggregated on. | -| max | DOUBLE | Max of all counter values for the counter name. | -| min | DOUBLE | Min of all counter values for the counter name. | -| dur | INT | Duration between the first and last counter value for the counter name. | -| wgt_avg | FLOAT | Weighted avergate of all the counter values for the counter name. | - -
- -
- -
- -**android_dvfs_counter_residency**. Aggregates dvfs counter slice for residency - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| name | STRING | Counter name. | -| value | DOUBLE | Counter value. | -| dur | INT | Counter duration. | -| pct | DOUBLE | Counter duration as a percentage of total duration. | - -
- -
- -### android.frames.jank_type - -#### Functions - -
- -**android_is_sf_jank_type** -\> BOOL. Categorizes whether the jank was caused by Surface Flinger - -
- -
- -
- -Returns BOOL: True when the jank type represents sf jank - -| Argument | Type | Description | -|---|---|---| -| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | - -
- -
- -
- -**android_is_app_jank_type** -\> BOOL. Categorizes whether the jank was caused by the app - -
- -
- -
- -Returns BOOL: True when the jank type represents app jank - -| Argument | Type | Description | -|---|---|---| -| jank_type | STRING | the jank type from args.display_value with key = "Jank type" | - -
- -
- -### android.frames.per_frame_metrics - -#### Views/Tables - -
- -**android_frames_overrun**. The amount by which each frame missed of hit its deadline - -
- -TABLE -The amount by which each frame missed of hit its deadline. Negative if the -deadline was not missed. Frames are considered janky if `overrun` is -positive. -Calculated as the difference between the end of the -`expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the -frame. -Availability: from S (API 31). -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id. | -| overrun | INT | Difference between `expected` and `actual` frame ends. Negative if frame didn't miss deadline. | - -
- -
- -
- -**android_frames_ui_time**. How much time did the frame's Choreographer callbacks take. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id | -| ui_time | INT | UI time duration | - -
- -
- -
- -**android_app_vsync_delay_per_frame**. App Vsync delay for a frame - -
- -TABLE -App Vsync delay for a frame. The time between the VSYNC-app signal and the -start of Choreographer work. -Calculated as time difference between the actual frame start (from -`actual_frame_timeline_slice`) and start of the `Choreographer#doFrame` -slice. -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id | -| app_vsync_delay | INT | App VSYNC delay. | - -
- -
- -
- -**android_cpu_time_per_frame** . How much time did the frame take across the UI Thread + RenderThread. Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice duration and summed durations of all `DrawFrame` slices associated with this frame. Availability: from N (API 24). For Googlers: more details in go/android-performance-metrics-glossary. - -
- -TABLE -How much time did the frame take across the UI Thread + RenderThread. -Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice -duration and summed durations of all `DrawFrame` slices associated with this -frame. -Availability: from N (API 24). -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id | -| app_vsync_delay | INT | Difference between actual timeline of the frame and `Choreographer#doFrame`. See `android_app_vsync_delay_per_frame` table for more details. | -| do_frame_dur | INT | Duration of `Choreographer#doFrame` slice. | -| draw_frame_dur | INT | Duration of `DrawFrame` slice. Summed duration of all `DrawFrame` slices, if more than one. See `android_frames_draw_frame` for more details. | -| cpu_time | INT | CPU time across the UI Thread + RenderThread. | - -
- -
- -
- -**android_frame_stats**. Aggregated stats of the frame. For Googlers: more details in go/android-performance-metrics-glossary. - -
- -TABLE -Aggregated stats of the frame. - -For Googlers: more details in go/android-performance-metrics-glossary. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id. | -| overrun | INT | The amount by which each frame missed of hit its deadline. See `android_frames_overrun` for details. | -| cpu_time | INT | How much time did the frame take across the UI Thread + RenderThread. | -| ui_time | INT | How much time did the frame's Choreographer callbacks take. | -| was_jank | BOOL | Was frame janky. | -| was_slow_frame | BOOL | CPU time of the frame took over 20ms. | -| was_big_jank | BOOL | CPU time of the frame took over 50ms. | -| was_huge_jank | BOOL | CPU time of the frame took over 200ms. | - -
- -
- -### android.frames.timeline - -#### Views/Tables - -
- -**android_frames_choreographer_do_frame** . All of the `Choreographer#doFrame` slices with their frame id. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | `slice.id` | -| frame_id | INT | Frame id | -| ui_thread_utid | INT | Utid of the UI thread | -| upid | INT | Upid of application process | -| ts | INT | Timestamp of the slice. | - -
- -
- -
- -**android_frames_draw_frame** . All of the `DrawFrame` slices with their frame id and render thread. There might be multiple DrawFrames slices for a single vsync (frame id). This happens when we are drawing multiple layers (e.g - -
- -TABLE -All of the `DrawFrame` slices with their frame id and render thread. -There might be multiple DrawFrames slices for a single vsync (frame id). -This happens when we are drawing multiple layers (e.g. status bar and -notifications). - -| Column | Type | Description | -|---|---|---| -| id | INT | `slice.id` | -| frame_id | INT | Frame id | -| render_thread_utid | INT | Utid of the render thread | -| upid | INT | Upid of application process | - -
- -
- -
- -**android_frames**. All slices related to one frame - -
- -TABLE -All slices related to one frame. Aggregates `Choreographer#doFrame`, -`DrawFrame`, `actual_frame_timeline_slice` and -`expected_frame_timeline_slice` slices. -See https://perfetto.dev/docs/data-sources/frametimeline for details. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id. | -| ts | INT | Timestamp of the frame. Start of the frame as defined by the start of "Choreographer#doFrame" slice and the same as the start of the frame in \`actual_frame_timeline_slice if present. | -| dur | INT | Duration of the frame, as defined by the duration of the corresponding `actual_frame_timeline_slice` or, if not present the time between the `ts` and the end of the final `DrawFrame`. | -| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | -| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | -| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | -| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | -| render_thread_utid | INT | `utid` of the render thread. | -| ui_thread_utid | INT | `utid` of the UI thread. | -| actual_frame_timeline_count | INT | Count of slices in `actual_frame_timeline_slice` related to this frame. | -| expected_frame_timeline_count | INT | Count of slices in `expected_frame_timeline_slice` related to this frame. | - -
- -
- -#### Table Functions - -
- -**android_first_frame_after** . Returns first frame after the provided timestamp - -
- -
- -
- -Returns first frame after the provided timestamp. The returning table has at -most one row. -Argument \| Type \| Description ---- \| --- \| --- -ts \| INT \| Timestamp. - -| Column | Type | Description | -|---|---|---| -| frame_id | INT | Frame id. | -| ts | INT | Start of the frame, the timestamp of the "Choreographer#doFrame" slice. | -| dur | INT | Duration of the frame. | -| do_frame_id | INT | `slice.id` of "Choreographer#doFrame" slice. | -| draw_frame_id | INT | `slice.id` of "DrawFrame" slice. | -| actual_frame_timeline_id | INT | `slice.id` from `actual_frame_timeline_slice` | -| expected_frame_timeline_id | INT | `slice.id` from `expected_frame_timeline_slice` | -| render_thread_utid | INT | `utid` of the render thread. | -| ui_thread_utid | INT | `utid` of the UI thread. | - -
- -
- -### android.freezer - -#### Views/Tables - -
- -**android_freezer_events**. All frozen processes and their frozen duration. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| upid | INT | Upid of frozen process | -| pid | INT | Pid of frozen process | -| ts | INT | Timestamp process was frozen. | -| dur | INT | Duration process was frozen for. | -| unfreeze_reason_int | INT | Unfreeze reason Integer. | -| unfreeze_reason_str | STRING | Unfreeze reason String. | - -
- -
- -### android.garbage_collection - -#### Views/Tables - -
- -**android_garbage_collection_events**. All Garbage collection events with a breakdown of the time spent and heap reclaimed. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| tid | INT | Tid of thread running garbage collection. | -| pid | INT | Pid of process running garbage collection. | -| utid | INT | Utid of thread running garbage collection. | -| upid | INT | Upid of process running garbage collection. | -| thread_name | STRING | Name of thread running garbage collection. | -| process_name | STRING | Name of process running garbage collection. | -| gc_type | STRING | Type of garbage collection. | -| is_mark_compact | INT | Whether gargage collection is mark compact or copying. | -| reclaimed_mb | DOUBLE | MB reclaimed after garbage collection. | -| min_heap_mb | DOUBLE | Minimum heap size in MB during garbage collection. | -| max_heap_mb | DOUBLE | Maximum heap size in MB during garbage collection. | -| gc_id | INT | Garbage collection id. | -| gc_ts | INT | Garbage collection timestamp. | -| gc_dur | INT | Garbage collection wall duration. | -| gc_running_dur | INT | Garbage collection duration spent executing on CPU. | -| gc_runnable_dur | INT | Garbage collection duration spent waiting for CPU. | -| gc_unint_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel on IO. | -| gc_unint_non_io_dur | INT | Garbage collection duration spent waiting in the Linux kernel without IO. | -| gc_int_dur | INT | Garbage collection duration spent waiting in interruptible sleep. | - -
- -
- -### android.gpu.frequency - -#### Views/Tables - -
- -**android_gpu_frequency**. GPU frequency counter per GPU. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp | -| dur | INT | Duration | -| gpu_id | INT | GPU id. Joinable with `gpu_counter_track.gpu_id`. | -| gpu_freq | INT | GPU frequency | - -
- -
- -### android.gpu.memory - -#### Views/Tables - -
- -**android_gpu_memory_per_process**. Counter for GPU memory per process with duration. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp | -| dur | INT | Duration | -| upid | INT | Upid of the process | -| gpu_memory | INT | GPU memory | - -
- -
- -### android.input - -#### Views/Tables - -
- -**android_input_events**. All input events with round trip latency breakdown - -
- -TABLE -All input events with round trip latency breakdown. Input delivery is socket based and every -input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure -latencies between: -1. Input dispatch event sent from OS. -2. Input dispatch event received in app. -3. Input ACK event sent from app. -4. Input ACk event received in OS. - -| Column | Type | Description | -|---|---|---| -| dispatch_latency_dur | INT | Duration from input dispatch to input received. | -| handling_latency_dur | INT | Duration from input received to input ACK sent. | -| ack_latency_dur | INT | Duration from input ACK sent to input ACK recieved. | -| total_latency_dur | INT | Duration from input dispatch to input event ACK received. | -| end_to_end_latency_dur | INT | Duration from input read to frame present time. Null if an input event has no associated frame event. | -| tid | INT | Tid of thread receiving the input event. | -| thread_name | STRING | Name of thread receiving the input event. | -| pid | INT | Pid of process receiving the input event. | -| process_name | STRING | Name of process receiving the input event. | -| event_type | STRING | Input event type. See InputTransport.h: InputMessage#Type | -| event_action | STRING | Input event action. | -| event_seq | STRING | Input event sequence number, monotonically increasing for an event channel and pid. | -| event_channel | STRING | Input event channel name. | -| input_event_id | STRING | Unique identifier for the input event. | -| read_time | INT | Timestamp input event was read by InputReader. | -| dispatch_track_id | INT | Thread track id of input event dispatching thread. | -| dispatch_ts | INT | Timestamp input event was dispatched. | -| dispatch_dur | INT | Duration of input event dispatch. | -| receive_track_id | INT | Thread track id of input event receiving thread. | -| receive_ts | INT | Timestamp input event was received. | -| receive_dur | INT | Duration of input event receipt. | -| frame_id | INT | Vsync Id associated with the input. Null if an input event has no associated frame event. | - -
- -
- -
- -**android_key_events**. Key events processed by the Android framework (from android.input.inputevent data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the trace entry | -| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | -| ts | INT | The timestamp of when the input event was processed by the system | -| arg_set_id | INT | Details of the input event parsed from the proto message | - -
- -
- -
- -**android_motion_events**. Motion events processed by the Android framework (from android.input.inputevent data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the trace entry | -| event_id | INT | The randomly-generated ID associated with each input event processed by Android Framework, used to track the event through the input pipeline | -| ts | INT | The timestamp of when the input event was processed by the system | -| arg_set_id | INT | Details of the input event parsed from the proto message | - -
- -
- -
- -**android_input_event_dispatch**. Input event dispatching information in Android (from android.input.inputevent data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the trace entry | -| event_id | INT | Event ID of the input event that was dispatched | -| arg_set_id | INT | Extra args parsed from the proto message | -| vsync_id | INT | Vsync ID that identifies the state of the windows during which the dispatch decision was made | -| window_id | INT | Window ID of the window receiving the event | - -
- -
- -### android.job_scheduler - -#### Views/Tables - -
- -**android_job_scheduler_events**. All scheduled jobs and their latencies. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| job_id | INT | Id of the scheduled job assigned by the app developer. | -| uid | INT | Uid of the process running the scheduled job. | -| package_name | STRING | Package name of the process running the scheduled job. | -| job_service_name | STRING | Service component name of the scheduled job. | -| track_id | INT | Thread track id of the job scheduler event slice. | -| id | INT | Slice id of the job scheduler event slice. | -| ts | INT | Timestamp the job was scheduled. | -| dur | INT | Duration of the scheduled job. | - -
- -
- -### android.memory.dmabuf - -#### Views/Tables - -
- -**android_dmabuf_allocs**. Track dmabuf allocations, re-attributing gralloc allocations to their source (if binder transactions to gralloc are recorded). - -
- -TABLE -Track dmabuf allocations, re-attributing gralloc allocations to their source -(if binder transactions to gralloc are recorded). - -| Column | Type | Description | -|---|---|---| -| ts | INT | timestamp of the allocation | -| buf_size | INT | allocation size (will be negative for release) | -| inode | INT | dmabuf inode | -| utid | INT | utid of thread responsible for the allocation if a dmabuf is allocated by gralloc we follow the binder transaction to the requesting thread (requires binder tracing) | -| tid | INT | tid of thread responsible for the allocation | -| thread_name | STRING | thread name | -| upid | INT | upid of process responsible for the allocation (matches utid) | -| pid | INT | pid of process responsible for the allocation | -| process_name | STRING | process name | - -
- -
- -### android.memory.heap_graph.dominator_tree - -#### Views/Tables - -
- -**heap_graph_dominator_tree**. All reachable heap graph objects, their immediate dominators and summary of their dominated sets. The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. Each reachable object is a node in the dominator tree, their immediate dominator is their parent node in the tree, and their dominated set is all their descendants in the tree - -
- -TABLE -All reachable heap graph objects, their immediate dominators and summary of -their dominated sets. -The heap graph dominator tree is calculated by stdlib graphs.dominator_tree. -Each reachable object is a node in the dominator tree, their immediate -dominator is their parent node in the tree, and their dominated set is all -their descendants in the tree. All size information come from the -heap_graph_object prelude table. - -| Column | Type | Description | -|---|---|---| -| id | INT | Heap graph object id. | -| idom_id | INT | Immediate dominator object id of the object. If the immediate dominator is the "super-root" (i.e. the object is a root or is dominated by multiple roots) then `idom_id` will be NULL. | -| dominated_obj_count | INT | Count of all objects dominated by this object, self inclusive. | -| dominated_size_bytes | INT | Total self_size of all objects dominated by this object, self inclusive. | -| dominated_native_size_bytes | INT | Total native_size of all objects dominated by this object, self inclusive. | -| depth | INT | Depth of the object in the dominator tree. Depth of root objects are 1. | - -
- -
- -### android.memory.heap_graph.heap_graph_class_aggregation - -#### Views/Tables - -
- -**android_heap_graph_class_aggregation**. Class-level breakdown of the java heap. Per type name aggregates the object stats and the dominator tree stats. - -
- -TABLE -Class-level breakdown of the java heap. -Per type name aggregates the object stats and the dominator tree stats. - -| Column | Type | Description | -|---|---|---| -| upid | INT | Process upid | -| graph_sample_ts | INT | Heap dump timestamp | -| type_id | INT | Class type id | -| type_name | STRING | Class name (deobfuscated if available) | -| is_libcore_or_array | BOOL | Is type an instance of a libcore object (java.\*) or array | -| obj_count | INT | Count of class instances | -| size_bytes | INT | Size of class instances | -| native_size_bytes | INT | Native size of class instances | -| reachable_obj_count | INT | Count of reachable class instances | -| reachable_size_bytes | INT | Size of reachable class instances | -| reachable_native_size_bytes | INT | Native size of reachable class instances | -| dominated_obj_count | INT | Count of all objects dominated by instances of this class Only applies to reachable objects | -| dominated_size_bytes | INT | Size of all objects dominated by instances of this class Only applies to reachable objects | -| dominated_native_size_bytes | INT | Native size of all objects dominated by instances of this class Only applies to reachable objects | - -
- -
- -### android.memory.process - -#### Views/Tables - -
- -**memory_oom_score_with_rss_and_swap_per_process**. Process memory and it's OOM adjuster scores - -
- -TABLE -Process memory and it's OOM adjuster scores. Detects transitions, each new -interval means that either the memory or OOM adjuster score of the process changed. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp the oom_adj score or memory of the process changed | -| dur | INT | Duration until the next oom_adj score or memory change of the process. | -| score | INT | oom adjuster score of the process. | -| bucket | STRING | oom adjuster bucket of the process. | -| upid | INT | Upid of the process having an oom_adj update. | -| process_name | STRING | Name of the process having an oom_adj update. | -| pid | INT | Pid of the process having an oom_adj update. | -| oom_adj_id | INT | Slice of the latest oom_adj update in the system_server. Alias of `slice.id`. | -| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | -| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | -| oom_adj_track_id | INT | Track of the latest oom_adj update in the system_server. Alias of `track.id`. | -| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | -| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | -| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | -| anon_rss | INT | Anon RSS counter value | -| file_rss | INT | File RSS counter value | -| shmem_rss | INT | Shared memory RSS counter value | -| rss | INT | Total RSS value. Sum of `anon_rss`, `file_rss` and `shmem_rss`. Returns value even if one of the values is NULL. | -| swap | INT | Swap counter value | -| anon_rss_and_swap | INT | Sum or `anon_rss` and `swap`. Returns value even if one of the values is NULL. | -| rss_and_swap | INT | Sum or `rss` and `swap`. Returns value even if one of the values is NULL. | - -
- -
- -### android.monitor_contention - -#### Views/Tables - -
- -**android_monitor_contention**. Contains parsed monitor contention slices. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| blocking_method | STRING | Name of the method holding the lock. | -| blocked_method | STRING | Blocked_method without arguments and return types. | -| short_blocking_method | STRING | Blocking_method without arguments and return types. | -| short_blocked_method | STRING | Blocked_method without arguments and return types. | -| blocking_src | STRING | File location of blocking_method in form . | -| blocked_src | STRING | File location of blocked_method in form . | -| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | -| blocked_utid | INT | Utid of thread holding the lock. | -| blocked_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_utid | INT | Utid of thread holding the lock. | -| blocking_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_tid | INT | Tid of thread holding the lock. | -| upid | INT | Upid of process experiencing lock contention. | -| process_name | STRING | Process name of process experiencing lock contention. | -| id | INT | Slice id of lock contention. | -| ts | INT | Timestamp of lock contention start. | -| dur | INT | Wall clock duration of lock contention. | -| monotonic_dur | INT | Monotonic clock duration of lock contention. | -| track_id | INT | Thread track id of blocked thread. | -| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | -| blocked_thread_tid | INT | Tid of the blocked thread | -| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | -| blocking_thread_tid | INT | Tid of thread holding the lock. | -| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | -| pid | INT | Pid of process experiencing lock contention. | - -
- -
- -
- -**android_monitor_contention_chain**. Contains parsed monitor contention slices with the parent-child relationships. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| parent_id | INT | Id of monitor contention slice blocking this contention. | -| blocking_method | STRING | Name of the method holding the lock. | -| blocked_method | STRING | Blocked_method without arguments and return types. | -| short_blocking_method | STRING | Blocking_method without arguments and return types. | -| short_blocked_method | STRING | Blocked_method without arguments and return types. | -| blocking_src | STRING | File location of blocking_method in form . | -| blocked_src | STRING | File location of blocked_method in form . | -| waiter_count | INT | Zero indexed number of threads trying to acquire the lock. | -| blocked_utid | INT | Utid of thread holding the lock. | -| blocked_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_utid | INT | Utid of thread holding the lock. | -| blocking_thread_name | STRING | Thread name of thread holding the lock. | -| blocking_tid | INT | Tid of thread holding the lock. | -| upid | INT | Upid of process experiencing lock contention. | -| process_name | STRING | Process name of process experiencing lock contention. | -| id | INT | Slice id of lock contention. | -| ts | INT | Timestamp of lock contention start. | -| dur | INT | Wall clock duration of lock contention. | -| monotonic_dur | INT | Monotonic clock duration of lock contention. | -| track_id | INT | Thread track id of blocked thread. | -| is_blocked_thread_main | INT | Whether the blocked thread is the main thread. | -| blocked_thread_tid | INT | Tid of the blocked thread | -| is_blocking_thread_main | INT | Whether the blocking thread is the main thread. | -| blocking_thread_tid | INT | Tid of thread holding the lock. | -| binder_reply_id | INT | Slice id of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_ts | INT | Timestamp of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_tid | INT | Tid of binder reply slice if lock contention was part of a binder txn. | -| pid | INT | Pid of process experiencing lock contention. | -| child_id | INT | Id of monitor contention slice blocked by this contention. | - -
- -
- -
- -**android_monitor_contention_chain_thread_state**. Note that we only span join the duration where the lock was actually held and contended. This can be less than the duration the lock was 'waited on' when a different waiter acquired the lock earlier than the first waiter. - -
- -TABLE -Note that we only span join the duration where the lock was actually held and contended. -This can be less than the duration the lock was 'waited on' when a different waiter acquired the -lock earlier than the first waiter. - -| Column | Type | Description | -|---|---|---| -| parent_id | None | Id of slice blocking the blocking_thread. | -| blocking_method | None | Name of the method holding the lock. | -| blocked_methhod | None | Name of the method trying to acquire the lock. | -| short_blocking_method | None | Blocking_method without arguments and return types. | -| short_blocked_method | None | Blocked_method without arguments and return types. | -| blocking_src | None | File location of blocking_method in form . | -| blocked_src | None | File location of blocked_method in form . | -| waiter_count | None | Zero indexed number of threads trying to acquire the lock. | -| blocking_utid | None | Utid of the blocking \|thread_state\|. | -| blocking_thread_name | None | Thread name of thread holding the lock. | -| upid | None | Upid of process experiencing lock contention. | -| process_name | None | Process name of process experiencing lock contention. | -| id | None | Slice id of lock contention. | -| ts | None | Timestamp of the blocking \|thread_state\|. | -| dur | None | Wall clock duration of lock contention. | -| monotonic_dur | None | Monotonic clock duration of lock contention. | -| track_id | None | Thread track id of blocked thread. | -| is_blocked_main_thread | None | Whether the blocked thread is the main thread. | -| is_blocking_main_thread | None | Whether the blocking thread is the main thread. | -| binder_reply_id | None | Slice id of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_ts | None | Timestamp of binder reply slice if lock contention was part of a binder txn. | -| binder_reply_tid | None | Tid of binder reply slice if lock contention was part of a binder txn. | -| state | None | Thread state of the blocking thread. | -| blocked_function | None | Blocked kernel function of the blocking thread. | - -
- -
- -
- -**android_monitor_contention_chain_thread_state_by_txn**. Aggregated thread_states on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention slice, it returns the aggregated sum of all the thread states on the blocking thread. Note that this data is only available for the first waiter on a lock. - -
- -VIEW -Aggregated thread_states on the 'blocking thread', the thread holding the lock. -This builds on the data from \|android_monitor_contention_chain\| and -for each contention slice, it returns the aggregated sum of all the thread states on the -blocking thread. - -Note that this data is only available for the first waiter on a lock. - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice id of the monitor contention. | -| thread_state | STRING | A \|thread_state\| that occurred in the blocking thread during the contention. | -| thread_state_dur | INT | Total time the blocking thread spent in the \|thread_state\| during contention. | -| thread_state_count | INT | Count of all times the blocking thread entered \|thread_state\| during the contention. | - -
- -
- -
- -**android_monitor_contention_chain_blocked_functions_by_txn**. Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. This builds on the data from \|android_monitor_contention_chain\| and for each contention, it returns the aggregated sum of all the kernel blocked function durations on the blocking thread. Note that this data is only available for the first waiter on a lock. - -
- -VIEW -Aggregated blocked_functions on the 'blocking thread', the thread holding the lock. -This builds on the data from \|android_monitor_contention_chain\| and -for each contention, it returns the aggregated sum of all the kernel -blocked function durations on the blocking thread. - -Note that this data is only available for the first waiter on a lock. - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice id of the monitor contention. | -| blocked_function | STRING | Blocked kernel function in a thread state in the blocking thread during the contention. | -| blocked_function_dur | INT | Total time the blocking thread spent in the \|blocked_function\| during the contention. | -| blocked_function_count | INT | Count of all times the blocking thread executed the \|blocked_function\| during the contention. | - -
- -
- -#### Functions - -
- -**android_extract_android_monitor_contention_blocking_thread** -\> STRING. Extracts the blocking thread from a slice name - -
- -
- -
- -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_blocking_tid** -\> INT. Extracts the blocking thread tid from a slice name - -
- -
- -
- -Returns INT: Blocking thread tid - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_blocking_method** -\> STRING. Extracts the blocking method from a slice name - -
- -
- -
- -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_short_blocking_method** -\> STRING. Extracts a shortened form of the blocking method name from a slice name. The shortened form discards the parameter and return types. - -
- -
- -
- -Extracts a shortened form of the blocking method name from a slice name. -The shortened form discards the parameter and return -types. -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_blocked_method** -\> STRING. Extracts the monitor contention blocked method from a slice name - -
- -
- -
- -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_short_blocked_method** -\> STRING. Extracts a shortened form of the monitor contention blocked method name from a slice name - -
- -
- -
- -Extracts a shortened form of the monitor contention blocked method name -from a slice name. The shortened form discards the parameter and return -types. -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_waiter_count** -\> INT. Extracts the number of waiters on the monitor from a slice name - -
- -
- -
- -Returns INT: Count of waiters on the lock - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_blocking_src** -\> STRING. Extracts the monitor contention blocking source location from a slice name - -
- -
- -
- -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -
- -**android_extract_android_monitor_contention_blocked_src** -\> STRING. Extracts the monitor contention blocked source location from a slice name - -
- -
- -
- -Returns STRING: Blocking thread - -| Argument | Type | Description | -|---|---|---| -| slice_name | STRING | Name of slice | - -
- -
- -#### Table Functions - -
- -**android_monitor_contention_graph** . Returns a DAG of all Java lock contentions in a process. Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. - -
- -
- -
- -Returns a DAG of all Java lock contentions in a process. -Each node in the graph is a pair. Each edge connects from a node waiting on a lock to a node holding a lock. The weights of each node represent the cumulative wall time the node blocked other nodes connected to it. Argument \| Type \| Description --- \| --- \| --- upid \| INT \| Upid of process to generate a lock graph for. - -| Column | Type | Description | -|---|---|---| -| pprof | BYTES | Pprof of lock graph. | - -
- -
- -### android.network_packets - -#### Views/Tables - -
- -**android_network_packets**. Android network packet events (from android.network_packets data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp in nanoseconds. | -| dur | INT | Duration (non-zero only in aggregate events) | -| track_name | STRING | The track name (interface and direction) | -| package_name | STRING | Traffic package source (or uid=$X if not found) | -| iface | STRING | Traffic interface name (linux interface name) | -| direction | STRING | Traffic direction ('Transmitted' or 'Received') | -| packet_count | INT | Number of packets in this event | -| packet_length | INT | Number of bytes in this event (wire size) | -| packet_transport | STRING | Transport used for traffic in this event | -| packet_tcp_flags | INT | TCP flags used by tcp frames in this event | -| socket_tag | STRING | The Android traffic tag of the network socket | -| socket_uid | INT | The Linux user id of the network socket | -| local_port | INT | The local port number (for udp or tcp only) | -| remote_port | INT | The remote port number (for udp or tcp only) | -| packet_icmp_type | INT | 1-byte ICMP type identifier. | -| packet_icmp_code | INT | 1-byte ICMP code identifier. | -| packet_tcp_flags_int | INT | Packet's tcp flags bitmask (e.g. FIN=0x1, SYN=0x2). | -| socket_tag_int | INT | Packet's socket tag as an integer. | - -
- -
- -### android.oom_adjuster - -#### Views/Tables - -
- -**android_oom_adj_intervals**. All oom adj state intervals across all processes along with the reason for the state update. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp the oom_adj score of the process changed | -| dur | INT | Duration until the next oom_adj score change of the process. | -| score | INT | oom_adj score of the process. | -| bucket | STRING | oom_adj bucket of the process. | -| upid | INT | Upid of the process having an oom_adj update. | -| process_name | STRING | Name of the process having an oom_adj update. | -| oom_adj_id | INT | Slice id of the latest oom_adj update in the system_server. | -| oom_adj_ts | INT | Timestamp of the latest oom_adj update in the system_server. | -| oom_adj_dur | INT | Duration of the latest oom_adj update in the system_server. | -| oom_adj_track_id | INT | Track id of the latest oom_adj update in the system_server | -| oom_adj_thread_name | STRING | Thread name of the latest oom_adj update in the system_server. | -| oom_adj_reason | STRING | Reason for the latest oom_adj update in the system_server. | -| oom_adj_trigger | STRING | Trigger for the latest oom_adj update in the system_server. | - -
- -
- -#### Functions - -
- -**android_oom_adj_score_to_bucket_name** -\> STRING. Converts an oom_adj score Integer to String sample name. One of: cached, background, job, foreground_service, bfgs, foreground and system. - -
- -
- -
- -Converts an oom_adj score Integer to String sample name. -One of: cached, background, job, foreground_service, bfgs, foreground and -system. -Returns STRING: Returns the sample bucket based on the oom score. - -| Argument | Type | Description | -|---|---|---| -| oom_score | INT | `oom_score` value | - -
- -
- -
- -**android_oom_adj_score_to_detailed_bucket_name** -\> STRING. Converts an oom_adj score Integer to String bucket name. Deprecated: use `android_oom_adj_score_to_bucket_name` instead. - -
- -
- -
- -Converts an oom_adj score Integer to String bucket name. -Deprecated: use `android_oom_adj_score_to_bucket_name` instead. -Returns STRING: Returns the oom_adj bucket. - -| Argument | Type | Description | -|---|---|---| -| value | INT | oom_adj score. | -| android_appid | INT | android_app id of the process. | - -
- -
- -### android.power_rails - -#### Views/Tables - -
- -**android_power_rails_counters**. Android power rails counters data. For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm NOTE: Requires dedicated hardware - table is only populated on Pixels. - -
- -TABLE -Android power rails counters data. -For details see: https://perfetto.dev/docs/data-sources/battery-counters#odpm -NOTE: Requires dedicated hardware - table is only populated on Pixels. - -| Column | Type | Description | -|---|---|---| -| id | INT | `counter.id` | -| ts | INT | Timestamp of the energy measurement. | -| dur | INT | Time until the next energy measurement. | -| power_rail_name | STRING | Power rail name. Alias of `counter_track.name`. | -| raw_power_rail_name | STRING | Raw power rail name. | -| energy_since_boot | DOUBLE | Energy accumulated by this rail since boot in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value`. | -| energy_since_boot_at_end | DOUBLE | Energy accumulated by this rail at next energy measurement in microwatt-seconds (uWs) (AKA micro-joules). Alias of `counter.value` of the next meaningful (with value change) counter value. | -| average_power | DOUBLE | Average power in mW (milliwatts) over between ts and the next energy measurement. | -| energy_delta | DOUBLE | The change of energy accumulated by this rails since the last measurement in microwatt-seconds (uWs) (AKA micro-joules). | -| track_id | INT | Power rail track id. Alias of `counter_track.id`. | -| value | DOUBLE | DEPRECATED. Use `energy_since_boot` instead. | - -
- -
- -### android.process_metadata - -#### Views/Tables - -
- -**android_process_metadata**. Data about packages running on the process. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| upid | INT | Process upid. | -| pid | INT | Process pid. | -| process_name | STRING | Process name. | -| uid | INT | Android app UID. | -| shared_uid | BOOL | Whether the UID is shared by multiple packages. | -| package_name | STRING | Name of the packages running in this process. | -| version_code | INT | Package version code. | -| debuggable | INT | Whether package is debuggable. | - -
- -
- -### android.screenshots - -#### Views/Tables - -
- -**android_screenshots**. Screenshot slices, used in perfetto UI. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice id. | -| ts | INT | Slice timestamp. | -| dur | INT | Slice duration, should be typically 0 since screeenshot slices are of instant type. | -| name | STRING | Slice name. | - -
- -
- -### android.services - -#### Views/Tables - -
- -**android_service_bindings**. All service bindings from client app to server app. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| client_oom_score | INT | OOM score of client process making the binding. | -| client_process | STRING | Name of client process making the binding. | -| client_thread | STRING | Name of client thread making the binding. | -| client_pid | INT | Pid of client process making the binding. | -| client_tid | INT | Tid of client process making the binding. | -| client_upid | INT | Upid of client process making the binding. | -| client_utid | INT | Utid of client thread making the binding. | -| client_ts | INT | Timestamp the client process made the request. | -| client_dur | INT | Duration of the client binding request. | -| server_oom_score | INT | OOM score of server process getting bound to. | -| server_process | STRING | Name of server process getting bound to | -| server_thread | STRING | Name of server thread getting bound to. | -| server_pid | INT | Pid of server process getting bound to. | -| server_tid | INT | Tid of server process getting bound to. | -| server_upid | INT | Upid of server process getting bound to. | -| server_utid | INT | Utid of server process getting bound to. | -| server_ts | INT | Timestamp the server process got bound to. | -| server_dur | INT | Duration of the server process handling the binding. | -| token | STRING | Unique binder identifier for the Service binding. | -| act | STRING | Intent action name for the service binding. | -| cmp | STRING | Intent component name for the service binding. | -| flg | STRING | Intent flag for the service binding. | -| bind_seq | INT | Monotonically increasing id for the service binding. | - -
- -
- -### android.slices - -#### Functions - -
- -**android_standardize_slice_name** -\> STRING. Some slice names have params in them - -
- -
- -
- -Some slice names have params in them. This functions removes them to make it -possible to aggregate by name. -Some examples are: -- Lock/monitor contention slices. The name includes where the lock -contention is in the code. That part is removed. -- DrawFrames/ooFrame. The name also includes the frame number. -- Apk/oat/dex loading: The name of the apk is removed -Returns STRING: Simplified name. - -| Argument | Type | Description | -|---|---|---| -| name | STRING | The raw slice name. | - -
- -
- -### android.startup.startups - -#### Views/Tables - -
- -**android_startups**. All activity startups in the trace by startup id. Populated by different scripts depending on the platform version/contents. - -
- -TABLE -All activity startups in the trace by startup id. -Populated by different scripts depending on the platform version/contents. - -| Column | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| ts | INT | Timestamp of startup start. | -| ts_end | INT | Timestamp of startup end. | -| dur | INT | Startup duration. | -| package | STRING | Package name. | -| startup_type | STRING | Startup type. | - -
- -
- -
- -**android_startup_processes**. Maps a startup to the set of processes that handled the activity start. The vast majority of cases should be a single process - -
- -TABLE -Maps a startup to the set of processes that handled the activity start. - -The vast majority of cases should be a single process. However it is -possible that the process dies during the activity startup and is respawned. - -| Column | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| upid | INT | Upid of process on which activity started. | -| startup_type | STRING | Type of the startup. | - -
- -
- -
- -**android_startup_threads**. Maps a startup to the set of threads on processes that handled the activity start. - -
- -VIEW -Maps a startup to the set of threads on processes that handled the -activity start. - -| Column | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| ts | INT | Timestamp of start. | -| dur | INT | Duration of startup. | -| upid | INT | Upid of process involved in startup. | -| utid | INT | Utid of the thread. | -| thread_name | STRING | Name of the thread. | -| is_main_thread | BOOL | Thread is a main thread. | - -
- -
- -
- -**android_thread_slices_for_all_startups**. All the slices for all startups in trace. Generally, this view should not be used - -
- -VIEW -All the slices for all startups in trace. - -Generally, this view should not be used. Instead, use one of the view functions related -to the startup slices which are created from this table. - -| Column | Type | Description | -|---|---|---| -| startup_ts | INT | Timestamp of startup. | -| startup_ts_end | INT | Timestamp of startup end. | -| startup_id | INT | Startup id. | -| utid | INT | UTID of thread with slice. | -| thread_name | STRING | Name of thread. | -| is_main_thread | BOOL | Whether it is main thread. | -| arg_set_id | INT | Arg set id. | -| slice_id | INT | Slice id. | -| slice_name | STRING | Name of slice. | -| slice_ts | INT | Timestamp of slice start. | -| slice_dur | INT | Slice duration. | - -
- -
- -#### Functions - -
- -**android_sum_dur_for_startup_and_slice** -\> INT. Returns duration of startup for slice name. Sums duration of all slices of startup with provided name. - -
- -
- -
- -Returns duration of startup for slice name. - -Sums duration of all slices of startup with provided name. -Returns INT: Sum of duration. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| slice_name | STRING | Slice name. | - -
- -
- -
- -**android_sum_dur_on_main_thread_for_startup_and_slice** -\> INT. Returns duration of startup for slice name on main thread. Sums duration of all slices of startup with provided name only on main thread. - -
- -
- -
- -Returns duration of startup for slice name on main thread. - -Sums duration of all slices of startup with provided name only on main thread. -Returns INT: Sum of duration. - -| Argument | Type | Description | -|---|---|---| -| startup_id | LONG | Startup id. | -| slice_name | STRING | Slice name. | - -
- -
- -#### Table Functions - -
- -**android_slices_for_startup_and_slice_name** . Given a startup id and GLOB for a slice name, returns matching slices with data. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| slice_name | STRING | Glob of the slice. | - -| Column | Type | Description | -|---|---|---| -| slice_id | INT | Id of the slice. | -| slice_name | STRING | Name of the slice. | -| slice_ts | INT | Timestamp of start of the slice. | -| slice_dur | INT | Duration of the slice. | -| thread_name | STRING | Name of the thread with the slice. | -| arg_set_id | INT | Arg set id. | - -
- -
- -
- -**android_binder_transaction_slices_for_startup** . Returns binder transaction slices for a given startup id with duration over threshold. - -
- -
- -
- -| Argument | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| threshold | DOUBLE | Only return slices with duration over threshold. | - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice id. | -| slice_dur | INT | Slice duration. | -| thread_name | STRING | Name of the thread with slice. | -| process | STRING | Name of the process with slice. | -| arg_set_id | INT | Arg set id. | -| is_main_thread | BOOL | Whether is main thread. | - -
- -
- -### android.startup.time_to_display - -#### Views/Tables - -
- -**android_startup_time_to_display**. Startup metric defintions, which focus on the observable time range: TTID - Time To Initial Display \* https://developer.android.com/topic/performance/vitals/launch-time#time-initial \* end of first RenderThread.DrawFrame - bindApplication TTFD - Time To Full Display \* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD \* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication Googlers: see go/android-performance-metrics-glossary for details. - -
- -TABLE -Startup metric defintions, which focus on the observable time range: -TTID - Time To Initial Display -\* https://developer.android.com/topic/performance/vitals/launch-time#time-initial -\* end of first RenderThread.DrawFrame - bindApplication -TTFD - Time To Full Display -\* https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD -\* end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication -Googlers: see go/android-performance-metrics-glossary for details. - -| Column | Type | Description | -|---|---|---| -| startup_id | INT | Startup id. | -| time_to_initial_display | INT | Time to initial display (TTID) | -| time_to_full_display | INT | Time to full display (TTFD) | -| ttid_frame_id | INT | `android_frames.frame_id` of frame for initial display | -| ttfd_frame_id | INT | `android_frames.frame_id` of frame for full display | -| upid | INT | `process.upid` of the startup | - -
- -
- -### android.statsd - -#### Views/Tables - -
- -**android_statsd_atoms**. Statsd atoms. A subset of the slice table containing statsd atom instant events. - -
- -VIEW -Statsd atoms. - -A subset of the slice table containing statsd atom instant events. - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique identifier for this slice. | -| type | STRING | The name of the "most-specific" child table containing this row. | -| ts | INT | The timestamp at the start of the slice (in nanoseconds). | -| dur | INT | The duration of the slice (in nanoseconds). | -| arg_set_id | INT | The id of the argument set associated with this slice. | -| thread_instruction_count | INT | The value of the CPU instruction counter at the start of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| thread_instruction_delta | INT | The change in value of the CPU instruction counter between the start and end of the slice. This column will only be populated if thread instruction collection is enabled with track_event. | -| track_id | INT | The id of the track this slice is located on. | -| category | STRING | The "category" of the slice. If this slice originated with track_event, this column contains the category emitted. Otherwise, it is likely to be null (with limited exceptions). | -| name | STRING | The name of the slice. The name describes what was happening during the slice. | -| depth | INT | The depth of the slice in the current stack of slices. | -| stack_id | INT | A unique identifier obtained from the names of all slices in this stack. This is rarely useful and kept around only for legacy reasons. | -| parent_stack_id | INT | The stack_id for the parent of this slice. Rarely useful. | -| parent_id | INT | The id of the parent (i.e. immediate ancestor) slice for this slice. | -| thread_ts | INT | The thread timestamp at the start of the slice. This column will only be populated if thread timestamp collection is enabled with track_event. | -| thread_dur | INT | The thread time used by this slice. This column will only be populated if thread timestamp collection is enabled with track_event. | - -
- -
- -### android.suspend - -#### Views/Tables - -
- -**android_suspend_state**. Table of suspended and awake slices. Selects either the minimal or full ftrace source depending on what's available, marks suspended periods, and complements them to give awake periods. - -
- -TABLE -Table of suspended and awake slices. - -Selects either the minimal or full ftrace source depending on what's -available, marks suspended periods, and complements them to give awake -periods. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Timestamp | -| dur | INT | Duration | -| power_state | STRING | 'awake' or 'suspended' | - -
- -
- -### android.winscope.inputmethod - -#### Views/Tables - -
- -**android_inputmethod_clients**. Android inputmethod clients state dumps (from android.inputmethod data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Dump id | -| ts | INT | Timestamp when the dump was triggered | -| arg_set_id | INT | Extra args parsed from the proto message | - -
- -
- -
- -**android_inputmethod_manager_service**. Android inputmethod manager service state dumps (from android.inputmethod data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Dump id | -| ts | INT | Timestamp when the dump was triggered | -| arg_set_id | INT | Extra args parsed from the proto message | - -
- -
- -
- -**android_inputmethod_service**. Android inputmethod service state dumps (from android.inputmethod data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Dump id | -| ts | INT | Timestamp when the dump was triggered | -| arg_set_id | INT | Extra args parsed from the proto message | - -
- -
- -### android.winscope.viewcapture - -#### Views/Tables - -
- -**android_viewcapture**. Android viewcapture (from android.viewcapture data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Snapshot id | -| ts | INT | Timestamp when the snapshot was triggered | -| arg_set_id | INT | Extra args parsed from the proto message | - -
- -
- -### android.winscope.windowmanager - -#### Views/Tables - -
- -**android_windowmanager**. Android WindowManager (from android.windowmanager data source). - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Snapshot id | -| ts | INT | Timestamp when the snapshot was triggered | -| arg_set_id | INT | Extra args parsed from the proto message | - -
- -
- -## Package: chrome - -### chrome.chrome_scrolls - -#### Views/Tables - -
- -**chrome_scrolls**. Defines slices for all of the individual scrolls in a trace based on the LatencyInfo-based scroll definition. NOTE: this view of top level scrolls is based on the LatencyInfo definition of a scroll, which differs subtly from the definition based on EventLatencies. TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ WebView instances - -
- -TABLE -Defines slices for all of the individual scrolls in a trace based on the -LatencyInfo-based scroll definition. - -NOTE: this view of top level scrolls is based on the LatencyInfo definition -of a scroll, which differs subtly from the definition based on -EventLatencies. -TODO(b/278684408): add support for tracking scrolls across multiple Chrome/ -WebView instances. Currently gesture_scroll_id unique within an instance, but -is not unique across multiple instances. Switching to an EventLatency based -definition of scrolls should resolve this. - -| Column | Type | Description | -|---|---|---| -| id | INT | The unique identifier of the scroll. | -| ts | INT | The start timestamp of the scroll. | -| dur | INT | The duration of the scroll. | -| gesture_scroll_begin_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the corresponding scroll id. | -| gesture_scroll_end_ts | INT | The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type / the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the corresponding scroll id. | - -
- -
- -### chrome.cpu_powerups - -#### Views/Tables - -
- -**chrome_cpu_power_slice**. The CPU power transitions in the trace. Power states are encoded as non-negative integers, with zero representing full-power operation and positive values representing increasingly deep sleep states. On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep state that the CPU enters while idle. - -
- -VIEW -The CPU power transitions in the trace. -Power states are encoded as non-negative integers, with zero representing -full-power operation and positive values representing increasingly deep -sleep states. - -On ARM systems, power state 1 represents the WFI (Wait For Interrupt) sleep -state that the CPU enters while idle. - -| Column | Type | Description | -|---|---|---| -| ts | INT | The timestamp at the start of the slice. | -| dur | INT | The duration of the slice. | -| cpu | INT | The CPU on which the transition occurred | -| power_state | INT | The power state that the CPU was in at time 'ts' for duration 'dur'. | -| previous_power_state | INT | The power state that the CPU was previously in. | -| powerup_id | INT | A unique ID for the CPU power-up. | - -
- -
- -
- -**chrome_cpu_power_first_sched_slice_after_powerup**. The Linux scheduler slices that executed immediately after a CPU power up. - -
- -TABLE -The Linux scheduler slices that executed immediately after a -CPU power up. - -| Column | Type | Description | -|---|---|---| -| ts | INT | The timestamp at the start of the slice. | -| dur | INT | The duration of the slice. | -| cpu | INT | The cpu on which the slice executed. | -| sched_id | INT | Id for the sched_slice table. | -| utid | INT | Unique id for the thread that ran within the slice. | -| previous_power_state | INT | The CPU's power state before this slice. | -| powerup_id | INT | A unique ID for the CPU power-up. | - -
- -
- -
- -**chrome_cpu_power_post_powerup_slice**. A table holding the slices that executed within the scheduler slice that ran on a CPU immediately after power-up. - -
- -TABLE -A table holding the slices that executed within the scheduler -slice that ran on a CPU immediately after power-up. - -| Column | Type | Description | -|---|---|---| -| ts | None | Timestamp of the resulting slice | -| dur | None | Duration of the slice. | -| cpu | None | The CPU the sched slice ran on. | -| utid | None | Unique thread id for the slice. | -| sched_id | None | 'id' field from the sched_slice table. | -| type | None | From the sched_slice table, always 'sched_slice'. | -| end_state | None | The ending state for the sched_slice | -| priority | None | The kernel thread priority | -| slice_id | None | Id of the top-level slice for this (sched) slice. | - -
- -
- -
- -**chrome_cpu_power_first_toplevel_slice_after_powerup**. The first top-level slice that ran after a CPU power-up. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| slice_id | INT | ID of the slice in the slice table. | -| previous_power_state | INT | The power state of the CPU prior to power-up. | - -
- -
- -### chrome.event_latency - -#### Views/Tables - -
- -**chrome_event_latencies**. All EventLatency slices. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice Id for the EventLatency scroll event. | -| name | STRING | Slice name. | -| ts | INT | The start timestamp of the scroll. | -| dur | INT | The duration of the scroll. | -| scroll_update_id | INT | The id of the scroll update event. | -| is_presented | BOOL | Whether this input event was presented. | -| event_type | STRING | EventLatency event type. | -| track_id | INT | Perfetto track this slice is found on. | - -
- -
- -
- -**chrome_gesture_scroll_events**. All scroll-related events (frames) including gesture scroll updates, begins and ends with respective scroll ids and start/end timestamps, regardless of being presented - -
- -TABLE -All scroll-related events (frames) including gesture scroll updates, begins -and ends with respective scroll ids and start/end timestamps, regardless of -being presented. This includes pinches that were presented. See b/315761896 -for context on pinches. - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice Id for the EventLatency scroll event. | -| name | STRING | Slice name. | -| ts | INT | The start timestamp of the scroll. | -| dur | INT | The duration of the scroll. | -| scroll_update_id | INT | The id of the scroll update event. | -| scroll_id | INT | The id of the scroll. | -| is_presented | BOOL | Whether this input event was presented. | -| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if SwapEndToPresentationCompositorFrame is missing due to b/247542163. | -| event_type | STRING | EventLatency event type. | -| track_id | INT | Perfetto track this slice is found on. | - -
- -
- -#### Functions - -
- -**chrome_get_most_recent_scroll_begin_id** -\> INT. Extracts scroll id for the EventLatency slice at `ts`. - -
- -
- -
- -Returns INT: The event_latency_id of the EventLatency slice with the type GESTURE_SCROLL_BEGIN that is the closest to `ts`. - -| Argument | Type | Description | -|---|---|---| -| ts | INT | Timestamp of the EventLatency slice to get the scroll id for. | - -
- -
- -### chrome.event_latency_description - -#### Views/Tables - -
- -**chrome_event_latency_stage_descriptions**. Source of truth of the descriptions of EventLatency stages. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the EventLatency stage. | -| description | STRING | A description of the EventLatency stage. | - -
- -
- -### chrome.histograms - -#### Views/Tables - -
- -**chrome_histograms**. A helper view on top of the histogram events emitted by Chrome. Requires "disabled-by-default-histogram_samples" Chrome category. - -
- -TABLE -A helper view on top of the histogram events emitted by Chrome. -Requires "disabled-by-default-histogram_samples" Chrome category. - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the histogram. | -| value | INT | The value of the histogram sample. | -| ts | INT | Alias of \|slice.ts\|. | -| thread_name | STRING | Thread name. | -| utid | INT | Utid of the thread. | -| tid | INT | Tid of the thread. | -| process_name | STRING | Process name. | -| upid | INT | Upid of the process. | -| pid | INT | Pid of the process. | - -
- -
- -### chrome.interactions - -#### Views/Tables - -
- -**chrome_interactions**. All critical user interaction events, including type and table with associated metrics. - -
- -TABLE -All critical user interaction events, including type and table with -associated metrics. - -| Column | Type | Description | -|---|---|---| -| scoped_id | INT | Identifier of the interaction; this is not guaranteed to be unique to the table - rather, it is unique within an individual interaction type. Combine with type to get a unique identifier in this table. | -| type | STRING | Type of this interaction, which together with scoped_id uniquely identifies this interaction. Also corresponds to a SQL table name containing more details specific to this type of interaction. | -| name | STRING | Interaction name - e.g. 'PageLoad', 'Tap', etc. Interactions will have unique metrics stored in other tables. | -| ts | INT | Timestamp of the CUI event. | -| dur | INT | Duration of the CUI event. | - -
- -
- -### chrome.metadata - -#### Functions - -
- -**chrome_hardware_class** -\> STRING. Returns hardware class of the device, often use to find device brand and model. - -
- -
- -
- -Returns hardware class of the device, often use to find device brand -and model. -Returns STRING: Hardware class name. - -
- -
- -### chrome.page_loads - -#### Views/Tables - -
- -**chrome_page_loads**. Chrome page loads, including associated high-level metrics and properties. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the navigation and Chrome browser process; this combination is unique to every individual navigation. | -| navigation_id | INT | ID of the navigation associated with the page load (i.e. the cross-document navigation in primary main frame which created this page's main document). Also note that navigation_id is specific to a given Chrome browser process, and not globally unique. | -| navigation_start_ts | INT | Timestamp of the start of navigation. | -| fcp | INT | Duration between the navigation start and the first contentful paint event (web.dev/fcp). | -| fcp_ts | INT | Timestamp of the first contentful paint. | -| lcp | INT | Duration between the navigation start and the largest contentful paint event (web.dev/lcp). | -| lcp_ts | INT | Timestamp of the largest contentful paint. | -| dom_content_loaded_event_ts | INT | Timestamp of the DomContentLoaded event: https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event | -| load_event_ts | INT | Timestamp of the window load event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event | -| mark_fully_loaded_ts | INT | Timestamp of the page self-reporting as fully loaded through the performance.mark('mark_fully_loaded') API. | -| mark_fully_visible_ts | INT | Timestamp of the page self-reporting as fully visible through the performance.mark('mark_fully_visible') API. | -| mark_interactive_ts | INT | Timestamp of the page self-reporting as fully interactive through the performance.mark('mark_interactive') API. | -| url | STRING | URL at the page load event. | -| browser_upid | INT | The unique process id (upid) of the browser process where the page load occurred. | - -
- -
- -### chrome.scroll_interactions - -#### Views/Tables - -
- -**chrome_scroll_interactions**. Top level scroll events, with metrics. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique id for an individual scroll. | -| name | STRING | Name of the scroll event. | -| ts | INT | Start timestamp of the scroll. | -| dur | INT | Duration of the scroll. | -| frame_count | INT | The total number of frames in the scroll. | -| vsync_count | INT | The total number of vsyncs in the scroll. | -| missed_vsync_max | INT | The maximum number of vsyncs missed during any and all janks. | -| missed_vsync_sum | INT | The total number of vsyncs missed during any and all janks. | -| delayed_frame_count | INT | The number of delayed frames. | -| predictor_janky_frame_count | INT | The number of frames that are deemed janky to the human eye after Chrome has applied its scroll prediction algorithm. | -| renderer_upid | INT | The process id this event occurred on. | - -
- -
- -### chrome.scroll_jank.predictor_error - -#### Views/Tables - -
- -**chrome_predictor_error**. The scrolling offsets and predictor jank values for the actual (applied) scroll events. - -
- -TABLE -The scrolling offsets and predictor jank values for the actual (applied) -scroll events. - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | -| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | -| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | -| present_ts | INT | Presentation timestamp. | -| delta_y | DOUBLE | The delta in raw coordinates between this presented EventLatency and the previous presented frame. | -| relative_offset_y | DOUBLE | The pixel offset of this presented EventLatency compared to the initial one. | -| prev_delta | DOUBLE | The delta in raw coordinates of the previous scroll update event. | -| next_delta | DOUBLE | The delta in raw coordinates of the subsequent scroll update event. | -| predictor_jank | DOUBLE | The jank value based on the discrepancy between scroll predictor coordinates and the actual deltas between scroll update events. | -| delta_threshold | DOUBLE | The threshold used to determine if jank occurred. | - -
- -
- -### chrome.scroll_jank.scroll_jank_cause_map - -#### Views/Tables - -
- -**chrome_scroll_jank_cause_descriptions**. Source of truth of the descriptions of EventLatency-based scroll jank causes. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| event_latency_stage | STRING | The name of the EventLatency stage. | -| cause_process | STRING | The process where the cause of scroll jank occurred. | -| cause_thread | STRING | The thread where the cause of scroll jank occurred. | -| cause_description | STRING | A description of the cause of scroll jank. | - -
- -
- -
- -**chrome_scroll_jank_causes_with_event_latencies**. Combined description of scroll jank cause and associated event latency stage. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| name | STRING | The name of the EventLatency stage. | -| description | STRING | Description of the EventLatency stage. | -| cause_process | STRING | The process name that may cause scroll jank. | -| cause_thread | STRING | The thread name that may cause scroll jank. The thread will be on the cause_process. | -| cause_description | STRING | Description of the cause of scroll jank on this process and thread. | - -
- -
- -### chrome.scroll_jank.scroll_jank_cause_utils - -#### Table Functions - -
- -**chrome_select_scroll_jank_cause_thread** . Function to retrieve the thread id of the thread on a particular process if there are any slices during a particular EventLatency slice duration; this upid/thread combination refers to a cause of Scroll Jank. - -
- -
- -
- -Function to retrieve the thread id of the thread on a particular process if -there are any slices during a particular EventLatency slice duration; this -upid/thread combination refers to a cause of Scroll Jank. -Argument \| Type \| Description ---- \| --- \| --- -event_latency_id \| INT \| The slice id of an EventLatency slice. -process_type \| STRING \| The process type that the thread is on: one of 'Browser', 'Renderer' or 'GPU'. -thread_name \| STRING \| The name of the thread. - -| Column | Type | Description | -|---|---|---| -| utid | INT | The utid associated with | - -
- -
- -### chrome.scroll_jank.scroll_jank_intervals - -#### Views/Tables - -
- -**chrome_janky_event_latencies_v3**. Selects EventLatency slices that correspond with janks in a scroll - -
- -TABLE -Selects EventLatency slices that correspond with janks in a scroll. This is -based on the V3 version of scroll jank metrics. - -| Column | Type | Description | -|---|---|---| -| id | INT | The slice id. | -| ts | INT | The start timestamp of the slice. | -| dur | INT | The duration of the slice. | -| track_id | INT | The track_id for the slice. | -| name | STRING | The name of the slice (EventLatency). | -| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | -| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | -| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | -| frame_jank_ts | INT | The start timestamp where frame presentation was delayed. | -| frame_jank_dur | INT | The duration in ms of the delay in frame presentation. | - -
- -
- -
- -**chrome_janky_frame_presentation_intervals**. Frame presentation interval is the delta between when the frame was supposed to be presented and when it was actually presented. - -
- -VIEW -Frame presentation interval is the delta between when the frame was supposed -to be presented and when it was actually presented. - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique id. | -| ts | INT | The start timestamp of the slice. | -| dur | INT | The duration of the slice. | -| delayed_frame_count | INT | How many vsyncs this frame missed its deadline by. | -| cause_of_jank | STRING | The stage of EventLatency that the caused the jank. | -| sub_cause_of_jank | STRING | The stage of cause_of_jank that caused the jank. | -| event_latency_id | INT | The id of the associated event latency in the slice table. | - -
- -
- -
- -**chrome_scroll_stats**. Scroll jank frame presentation stats for individual scrolls. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | Id of the individual scroll. | -| frame_count | INT | The number of frames in the scroll. | -| missed_vsyncs | INT | The number of missed vsyncs in the scroll. | -| presented_frame_count | INT | The number presented frames in the scroll. | -| janky_frame_count | INT | The number of janky frames in the scroll. | -| janky_frame_percent | FLOAT | The % of frames that janked in the scroll. | - -
- -
- -
- -**chrome_scroll_jank_intervals_v3**. Defines slices for all of janky scrolling intervals in a trace. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | The unique identifier of the janky interval. | -| ts | INT | The start timestamp of the janky interval. | -| dur | INT | The duration of the janky interval. | - -
- -
- -### chrome.scroll_jank.scroll_jank_v3 - -#### Views/Tables - -
- -**chrome_gesture_scroll_updates**. Grabs all gesture updates with respective scroll ids and start/end timestamps, regardless of being presented. - -
- -TABLE -Grabs all gesture updates with respective scroll ids and start/end -timestamps, regardless of being presented. - -| Column | Type | Description | -|---|---|---| -| ts | INT | The start timestamp of the scroll. | -| dur | INT | The duration of the scroll. | -| id | INT | Slice id for the scroll. | -| scroll_update_id | INT | The id of the scroll update event. | -| scroll_id | INT | The id of the scroll. | -| is_presented | BOOL | Whether this input event was presented. | -| presentation_timestamp | INT | Frame presentation timestamp aka the timestamp of the SwapEndToPresentationCompositorFrame substage. | -| event_type | STRING | EventLatency event type. | - -
- -
- -
- -**chrome_presented_gesture_scrolls**. Scroll updates, corresponding to all input events that were converted to a presented scroll update. - -
- -TABLE -Scroll updates, corresponding to all input events that were converted to a -presented scroll update. - -| Column | Type | Description | -|---|---|---| -| id | INT | Minimum slice id for input presented in this frame, the non-presented input. | -| ts | INT | The start timestamp for producing the frame. | -| dur | INT | The duration between producing and presenting the frame. | -| last_presented_input_ts | INT | The timestamp of the last input that arrived and got presented in the frame. | -| scroll_update_id | INT | The id of the scroll update event, a unique identifier to the gesture. | -| scroll_id | INT | The id of the ongoing scroll. | -| presentation_timestamp | INT | Frame presentation timestamp. | -| event_type | STRING | EventLatency event type. | - -
- -
- -
- -**chrome_scroll_updates_with_deltas**. Associate every trace_id with it's perceived delta_y on the screen after prediction. - -
- -TABLE -Associate every trace_id with it's perceived delta_y on the screen after -prediction. - -| Column | Type | Description | -|---|---|---| -| scroll_update_id | INT | The id of the scroll update event. | -| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | - -
- -
- -
- -**chrome_full_frame_view**. Obtain the subset of input events that were fully presented. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the frame. | -| ts | INT | Start timestamp of the frame. | -| last_presented_input_ts | INT | The timestamp of the last presented input. | -| scroll_id | INT | ID of the associated scroll. | -| scroll_update_id | INT | ID of the associated scroll update. | -| event_latency_id | INT | ID of the associated EventLatency. | -| dur | INT | Duration of the associated EventLatency. | -| presentation_timestamp | INT | Frame presentation timestamp. | - -
- -
- -
- -**chrome_full_frame_delta_view**. Join deltas with EventLatency data. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the frame. | -| ts | INT | Start timestamp of the frame. | -| scroll_id | INT | ID of the associated scroll. | -| scroll_update_id | INT | ID of the associated scroll update. | -| last_presented_input_ts | INT | The timestamp of the last presented input. | -| delta_y | DOUBLE | The perceived delta_y on the screen post prediction. | -| event_latency_id | INT | ID of the associated EventLatency. | -| dur | INT | Duration of the associated EventLatency. | -| presentation_timestamp | INT | Frame presentation timestamp. | - -
- -
- -
- -**chrome_merged_frame_view**. Group all gestures presented at the same timestamp together in a single row. - -
- -TABLE -Group all gestures presented at the same timestamp together in -a single row. - -| Column | Type | Description | -|---|---|---| -| id | INT | ID of the frame. | -| max_start_ts | INT | The timestamp of the last presented input. | -| min_start_ts | INT | The earliest frame start timestamp. | -| scroll_id | INT | ID of the associated scroll. | -| scroll_update_id | INT | ID of the associated scroll update. | -| encapsulated_scroll_ids | STRING | All scroll updates associated with the frame presentation timestamp. | -| total_delta | DOUBLE | Sum of all perceived delta_y values at the frame presentation timestamp. | -| segregated_delta_y | STRING | Lists all of the perceived delta_y values at the frame presentation timestamp. | -| event_latency_id | INT | ID of the associated EventLatency. | -| dur | INT | Maximum duration of the associated EventLatency. | -| presentation_timestamp | INT | Frame presentation timestamp. | - -
- -
- -
- -**chrome_frame_info_with_delay**. View contains all chrome presented frames during gesture updates while calculating delay since last presented which usually should equal to \|VSYNC_INTERVAL\| if no jank is present. - -
- -TABLE -View contains all chrome presented frames during gesture updates -while calculating delay since last presented which usually should -equal to \|VSYNC_INTERVAL\| if no jank is present. - -| Column | Type | Description | -|---|---|---| -| id | INT | gesture scroll slice id. | -| max_start_ts | INT | OS timestamp of the last touch move arrival within a frame. | -| min_start_ts | INT | OS timestamp of the first touch move arrival within a frame. | -| scroll_id | INT | The scroll which the touch belongs to. | -| scroll_update_id | INT | ID of the associated scroll update. | -| encapsulated_scroll_ids | STRING | Trace ids of all frames presented in at this vsync. | -| total_delta | DOUBLE | Summation of all delta_y of all gesture scrolls in this frame. | -| segregated_delta_y | STRING | All delta y of all gesture scrolls comma separated, summing those gives \|total_delta\|. | -| event_latency_id | INT | Event latency id of the presented frame. | -| dur | INT | Duration of the EventLatency. | -| presentation_timestamp | INT | Timestamp at which the frame was shown on the screen. | -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, usually equals \|VSYNC\| if no frame drops happened. | -| delay_since_last_input | DOUBLE | Difference in OS timestamps of inputs in the current and the previous frame. | -| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | - -
- -
- -
- -**chrome_vsyncs**. Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the minimum delay between frames larger than zero. TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. - -
- -TABLE -Calculate \|VSYNC_INTERVAL\| as the lowest vsync seen in the trace or the -minimum delay between frames larger than zero. - -TODO(\~M130): Remove the lowest vsync since we should always have vsync_interval_ms. - -| Column | Type | Description | -|---|---|---| -| vsync_interval | DOUBLE | The lowest delay between frames larger than zero. | - -
- -
- -
- -**chrome_janky_frames_no_cause**. Filter the frame view only to frames that had missed vsyncs. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | -| event_latency_id | INT | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | INT | The scroll corresponding to this frame. | -| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | - -
- -
- -
- -**chrome_janky_frames_no_subcause**. Janky frame information including the jank cause. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | -| event_latency_id | INT | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | INT | The scroll corresponding to this frame. | -| prev_event_latency_id | INT | The event latency id that will be used as a reference to determine the jank cause. | -| cause_id | INT | Id of the slice corresponding to the offending stage. | - -
- -
- -
- -**chrome_janky_frames**. Finds all causes of jank for all janky frames, and a cause of sub jank if the cause of jank was GPU related. - -
- -TABLE -Finds all causes of jank for all janky frames, and a cause of sub jank -if the cause of jank was GPU related. - -| Column | Type | Description | -|---|---|---| -| cause_of_jank | STRING | The reason the Vsync was missed. | -| sub_cause_of_jank | STRING | Further breakdown if the root cause was GPU related. | -| delay_since_last_frame | DOUBLE | Time elapsed since the previous frame was presented, will be more than \|VSYNC\| in this view. | -| event_latency_id | INT | Event latency id of the presented frame. | -| vsync_interval | DOUBLE | Vsync interval at the time of recording the trace. | -| hardware_class | STRING | Device brand and model. | -| scroll_id | INT | The scroll corresponding to this frame. | - -
- -
- -
- -**chrome_unique_frame_presentation_ts**. Counting all unique frame presentation timestamps. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| presentation_timestamp | INT | The unique frame presentation timestamp. | - -
- -
- -
- -**chrome_janky_frames_percentage**. Dividing missed frames over total frames to get janky frame percentage. This represents the v3 scroll jank metrics. Reflects Event.Jank.DelayedFramesPercentage UMA metric. - -
- -TABLE -Dividing missed frames over total frames to get janky frame percentage. -This represents the v3 scroll jank metrics. -Reflects Event.Jank.DelayedFramesPercentage UMA metric. - -| Column | Type | Description | -|---|---|---| -| delayed_frame_percentage | FLOAT | The percent of missed frames relative to total frames - aka the percent of janky frames. | - -
- -
- -
- -**chrome_frames_per_scroll**. Number of frames and janky frames per scroll. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | The ID of the scroll. | -| num_frames | INT | The number of frames in the scroll. | -| num_janky_frames | INT | The number of delayed/janky frames. | -| scroll_jank_percentage | DOUBLE | The percentage of janky frames relative to total frames. | - -
- -
- -
- -**chrome_causes_per_scroll**. Scroll jank causes per scroll. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | The ID of the scroll. | -| max_delay_since_last_frame | DOUBLE | The maximum time a frame was delayed after the presentation of the previous frame. | -| vsync_interval | DOUBLE | The expected vsync interval. | -| scroll_jank_causes | BYTES | A proto amalgamation of each scroll jank cause including cause name, sub cause and the duration of the delay since the previous frame was presented. | - -
- -
- -### chrome.scroll_jank.scroll_jank_v3_cause - -#### Functions - -
- -**chrome_get_v3_jank_cause_id** -\> LONG. Given two slice Ids A and B, find the maximum difference between the durations of it's direct children with matching names for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) and slice B has children named (X, Y) with durations of (9, 9), the function will return the slice id of the slice named Z that is A's child, as no matching slice named Z was found under B, making 5 - 0 = 5 the maximum delta between both slice's direct children - -
- -
- -
- -Given two slice Ids A and B, find the maximum difference -between the durations of it's direct children with matching names -for example if slice A has children named (X, Y, Z) with durations of (10, 10, 5) -and slice B has children named (X, Y) with durations of (9, 9), the function will return -the slice id of the slice named Z that is A's child, as no matching slice named Z was found -under B, making 5 - 0 = 5 the maximum delta between both slice's direct children -Returns LONG: The slice id of the breakdown that has the maximum duration delta. - -| Argument | Type | Description | -|---|---|---| -| janky_slice_id | LONG | The slice id of the parent slice that we want to cause among it's children. | -| prev_slice_id | LONG | The slice id of the parent slice that's the reference in comparison to \|janky_slice_id\|. | - -
- -
- -### chrome.scroll_jank.scroll_offsets - -#### Views/Tables - -
- -**chrome_scroll_input_offsets**. The raw coordinates and pixel offsets for all input events which were part of a scroll. - -
- -TABLE -The raw coordinates and pixel offsets for all input events which were part of -a scroll. - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | -| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | -| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | -| ts | INT | Timestamp the of the scroll input event. | -| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | -| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | - -
- -
- -
- -**chrome_presented_scroll_offsets**. The scrolling offsets for the actual (applied) scroll events - -
- -TABLE -The scrolling offsets for the actual (applied) scroll events. These are not -necessarily inclusive of all user scroll events, rather those scroll events -that are actually processed. - -| Column | Type | Description | -|---|---|---| -| scroll_id | INT | An ID that ties all EventLatencies in a particular scroll. (implementation note: This is the EventLatency TraceId of the GestureScrollbegin). | -| event_latency_slice_id | INT | An ID for this particular EventLatency regardless of it being presented or not. | -| scroll_update_id | INT | An ID that ties this \|event_latency_id\| with the Trace Id (another event_latency_id) that it was presented with. | -| ts | INT | Presentation timestamp. | -| delta_y | DOUBLE | The delta in raw coordinates between this scroll update event and the previous. | -| relative_offset_y | DOUBLE | The pixel offset of this scroll update event compared to the initial one. | - -
- -
- -### chrome.scroll_jank.utils - -#### Table Functions - -
- -**chrome_select_long_task_slices** . Extract mojo information for the long-task-tracking scenario for specific names - -
- -
- -
- -Extract mojo information for the long-task-tracking scenario for specific -names. For example, LongTaskTracker slices may have associated IPC -metadata, or InterestingTask slices for input may have associated IPC to -determine whether the task is fling/etc. -Argument \| Type \| Description ---- \| --- \| --- -name \| STRING \| The name of slice. - -| Column | Type | Description | -|---|---|---| -| interface_name | STRING | Name of the interface of the IPC call. | -| ipc_hash | INT | Hash of the IPC call. | -| message_type | STRING | Message type (e.g. reply). | -| id | INT | The slice id. | - -
- -
- -### chrome.speedometer - -#### Views/Tables - -
- -**chrome_speedometer_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async - -
- -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the measure slice | -| dur | INT | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | INT | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -
- -
- -
- -**chrome_speedometer_iteration**. Slice that covers one Speedometer iteration. Depending on the Speedometer version these slices might need to be estimated as older versions of Speedometer to not emit marks for this interval - -
- -TABLE -Slice that covers one Speedometer iteration. -Depending on the Speedometer version these slices might need to be estimated -as older versions of Speedometer to not emit marks for this interval. The -metrics associated are the same ones Speedometer would output, but note we -use ns precision (Speedometer uses \~100us) so the actual values might differ -a bit. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the iteration | -| dur | INT | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | INT | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -
- -
- -#### Functions - -
- -**chrome_speedometer_score** -\> DOUBLE. Returns the Speedometer score for all iterations in the trace - -
- -
- -
- -Returns DOUBLE: Speedometer score - -
- -
- -
- -**chrome_speedometer_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 - -
- -
- -
- -Returns INT: Renderer main utid - -
- -
- -### chrome.speedometer_2_1 - -#### Views/Tables - -
- -**chrome_speedometer_2_1_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async sync is the time between the start and sync-end marks, async is the time between the sync-end and async-end marks. - -
- -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async -sync is the time between the start and sync-end marks, async is the time -between the sync-end and async-end marks. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the measure slice | -| dur | INT | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | INT | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -
- -
- -
- -**chrome_speedometer_2_1_iteration**. Slice that covers one Speedometer iteration. This slice is actually estimated as a default Speedometer run will not emit marks to cover this interval - -
- -TABLE -Slice that covers one Speedometer iteration. -This slice is actually estimated as a default Speedometer run will not emit -marks to cover this interval. The metrics associated are the same ones -Speedometer would output, but note we use ns precision (Speedometer uses -\~100us) so the actual values might differ a bit. Also note Speedometer -returns the values in ms these here and in ns. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the iteration | -| dur | INT | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | INT | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -
- -
- -#### Functions - -
- -**chrome_speedometer_2_1_score** -\> DOUBLE. Returns the Speedometer 2.1 score for all iterations in the trace - -
- -
- -
- -Returns DOUBLE: Speedometer 2.1 score - -
- -
- -
- -**chrome_speedometer_2_1_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 2.1 - -
- -
- -
- -Returns INT: Renderer main utid - -
- -
- -### chrome.speedometer_3 - -#### Views/Tables - -
- -**chrome_speedometer_3_measure**. Augmented slices for Speedometer measurements. These are the intervals of time Speedometer uses to compute the final score. There are two intervals that are measured for every test: sync and async. - -
- -TABLE -Augmented slices for Speedometer measurements. -These are the intervals of time Speedometer uses to compute the final score. -There are two intervals that are measured for every test: sync and async. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the measure slice | -| dur | INT | Duration of the measure slice | -| name | STRING | Full measure name | -| iteration | INT | Speedometer iteration the slice belongs to. | -| suite_name | STRING | Suite name | -| test_name | STRING | Test name | -| measure_type | STRING | Type of the measure (sync or async) | - -
- -
- -
- -**chrome_speedometer_3_iteration**. Slice that covers one Speedometer iteration. The metrics associated are the same ones Speedometer would output, but note we use ns precision (Speedometer uses \~100us) so the actual values might differ a bit. - -
- -TABLE -Slice that covers one Speedometer iteration. -The metrics associated are the same ones -Speedometer would output, but note we use ns precision (Speedometer uses -\~100us) so the actual values might differ a bit. - -| Column | Type | Description | -|---|---|---| -| ts | INT | Start timestamp of the iteration | -| dur | INT | Duration of the iteration | -| name | STRING | Iteration name | -| iteration | INT | Iteration number | -| geomean | DOUBLE | Geometric mean of the suite durations for this iteration. | -| score | DOUBLE | Speedometer score for this iteration (The total score for a run in the average of all iteration scores). | - -
- -
- -#### Functions - -
- -**chrome_speedometer_3_score** -\> DOUBLE. Returns the Speedometer 3 score for all iterations in the trace - -
- -
- -
- -Returns DOUBLE: Speedometer 3 score - -
- -
- -
- -**chrome_speedometer_3_renderer_main_utid** -\> INT. Returns the utid for the main thread that ran Speedometer 3 - -
- -
- -
- -Returns INT: Renderer main utid - -
- -
- -### chrome.startups - -#### Views/Tables - -
- -**chrome_startups**. Chrome startups, including launch cause. - -
- -TABLE - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique ID | -| activity_id | INT | Chrome Activity event id of the launch. | -| name | STRING | Name of the launch start event. | -| startup_begin_ts | INT | Timestamp that the startup occurred. | -| first_visible_content_ts | INT | Timestamp to the first visible content. | -| launch_cause | STRING | Launch cause. See Startup.LaunchCauseType in chrome_track_event.proto. | -| browser_upid | INT | Process ID of the Browser where the startup occurred. | - -
- -
- -### chrome.tasks - -#### Views/Tables - -
- -**chrome_java_views**. A list of slices corresponding to operations on interesting (non-generic) Chrome Java views - -
- -VIEW -A list of slices corresponding to operations on interesting (non-generic) -Chrome Java views. The view is considered interested if it's not a system -(ContentFrameLayout) or generic library (CompositorViewHolder) views. - -TODO(altimin): Add "columns_from slice" annotation. -TODO(altimin): convert this to EXTEND_TABLE when it becomes available. - -| Column | Type | Description | -|---|---|---| -| filtered_name | STRING | Name of the view. | -| is_software_screenshot | BOOL | Whether this slice is a part of non-accelerated capture toolbar screenshot. | -| is_hardware_screenshot | BOOL | Whether this slice is a part of accelerated capture toolbar screenshot. | -| slice_id | INT | Slice id. | - -
- -
- -
- -**chrome_scheduler_tasks**. A list of tasks executed by Chrome scheduler. - -
- -VIEW - -| Column | Type | Description | -|---|---|---| -| id | INT | Slice id. | -| type | STRING | Type. | -| name | STRING | Name of the task. | -| ts | INT | Timestamp. | -| dur | INT | Duration. | -| utid | INT | Utid of the thread this task run on. | -| thread_name | STRING | Name of the thread this task run on. | -| upid | INT | Upid of the process of this task. | -| process_name | STRING | Name of the process of this task. | -| track_id | INT | Same as slice.track_id. | -| category | STRING | Same as slice.category. | -| depth | INT | Same as slice.depth. | -| parent_id | INT | Same as slice.parent_id. | -| arg_set_id | INT | Same as slice.arg_set_id. | -| thread_ts | INT | Same as slice.thread_ts. | -| thread_dur | INT | Same as slice.thread_dur. | -| posted_from | STRING | Source location where the PostTask was called. | - -
- -
- -
- -**chrome_tasks**. A list of "Chrome tasks": top-level execution units (e.g - -
- -VIEW -A list of "Chrome tasks": top-level execution units (e.g. scheduler tasks / -IPCs / system callbacks) run by Chrome. For a given thread, the slices -corresponding to these tasks will not intersect. - -| Column | Type | Description | -|---|---|---| -| id | INT | Id for the given task, also the id of the slice this task corresponds to. | -| name | STRING | Name for the given task. | -| task_type | STRING | Type of the task (e.g. "scheduler"). | -| thread_name | STRING | Thread name. | -| utid | INT | Utid. | -| process_name | STRING | Process name. | -| upid | INT | Upid. | -| ts | INT | Alias of \|slice.ts\|. | -| dur | INT | Alias of \|slice.dur\|. | -| track_id | INT | Alias of \|slice.track_id\|. | -| category | STRING | Alias of \|slice.category\|. | -| arg_set_id | INT | Alias of \|slice.arg_set_id\|. | -| thread_ts | INT | Alias of \|slice.thread_ts\|. | -| thread_dur | INT | Alias of \|slice.thread_dur\|. | -| full_name | STRING | STRING Legacy alias for \|name\|. | - -
- -
- -### chrome.vsync_intervals - -#### Views/Tables - -
- -**chrome_vsync_intervals**. A simple table that checks the time between VSync (this can be used to determine if we're refreshing at 90 FPS or 60 FPS). Note: In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. - -
- -TABLE -A simple table that checks the time between VSync (this can be used to -determine if we're refreshing at 90 FPS or 60 FPS). - -> [!NOTE] -> **Note:** In traces without the "Java" category there will be no VSync TraceEvents and this table will be empty. - -| Column | Type | Description | -|---|---|---| -| slice_id | INT | Slice id of the vsync slice. | -| ts | INT | Timestamp of the vsync slice. | -| dur | INT | Duration of the vsync slice. | -| track_id | INT | Track id of the vsync slice. | -| time_to_next_vsync | INT | Duration until next vsync arrives. | - -
- -
- -#### Functions - -
- -**chrome_calculate_avg_vsync_interval** -\> FLOAT. Function: compute the average Vysnc interval of the gesture (hopefully this would be either 60 FPS for the whole gesture or 90 FPS but that isnt always the case) on the given time segment. If the trace doesnt contain the VSync TraceEvent we just fall back on assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which corresponds to 16 ms or 60 FPS). - -
- -
- -
- -Function: compute the average Vysnc interval of the -gesture (hopefully this would be either 60 FPS for the whole gesture or 90 -FPS but that isnt always the case) on the given time segment. -If the trace doesnt contain the VSync TraceEvent we just fall back on -assuming its 60 FPS (this is the 1.6e+7 in the COALESCE which -corresponds to 16 ms or 60 FPS). -Returns FLOAT: The average vsync interval on this time segment or 1.6e+7, if trace doesn't contain the VSync TraceEvent. - -| Argument | Type | Description | -|---|---|---| -| begin_ts | LONG | Interval start time. | -| end_ts | LONG | Interval end time. | - -
- -
- -### chrome.web_content_interactions - -#### Views/Tables - -
- -**chrome_web_content_interactions**. Chrome web content interactions (InteractionToFirstPaint), including associated high-level metrics and properties. Multiple events may occur for the same interaction; each row in this table represents the primary (longest) event for the interaction. Web content interactions are discrete, as opposed to sustained (e.g. scrolling); and only occur with the web content itself, as opposed to other parts of Chrome (e.g - -
- -TABLE -Chrome web content interactions (InteractionToFirstPaint), including -associated high-level metrics and properties. - -Multiple events may occur for the same interaction; each row in this table -represents the primary (longest) event for the interaction. - -Web content interactions are discrete, as opposed to sustained (e.g. -scrolling); and only occur with the web content itself, as opposed to other -parts of Chrome (e.g. omnibox). Interaction events include taps, clicks, -keyboard input (typing), and drags. - -| Column | Type | Description | -|---|---|---| -| id | INT | Unique id for this interaction. | -| ts | INT | Start timestamp of the event. Because multiple events may occur for the same interaction, this is the start timestamp of the longest event. | -| dur | INT | Duration of the event. Because multiple events may occur for the same interaction, this is the duration of the longest event. | -| interaction_type | STRING | The interaction type. | -| total_duration_ms | INT | The total duration of all events that occurred for the same interaction. | -| renderer_upid | INT | The process id this event occurred on. | - -
- -
- -## Package: counters - -### counters.intervals - -#### Macros - -
- -**counter_leading_intervals** . For a given counter timeline (e.g - -
- -
- -
- -For a given counter timeline (e.g. a single counter track), returns -intervals of time where the counter has the same value. - -Intervals are computed in a "forward-looking" way. That is, if a counter -changes value at some timestamp, it's assumed it *just* reached that -value and it should continue to have that value until the next -value change. The final value is assumed to hold until the very end of -the trace. - -For example, suppose we have the following data: -`ts=0, value=10, track_id=1 -ts=0, value=10, track_id=2 -ts=10, value=10, track_id=1 -ts=10, value=20, track_id=2 -ts=20, value=30, track_id=1 -[end of trace at ts = 40]` - -Then this macro will generate the following intervals: -`ts=0, dur=20, value=10, track_id=1 -ts=20, dur=10, value=30, track_id=1 -ts=0, dur=10, value=10, track_id=2 -ts=10, dur=30, value=20, track_id=2` -Returns: TableOrSubquery, Table with the schema (id UINT32, ts UINT64, dur UINT64, track_id UINT64, value DOUBLE, next_value DOUBLE, delta_value DOUBLE). - -| Argument | Type | Description | -|---|---|---| -| counter_table | TableOrSubquery | A table/view/subquery corresponding to a "counter-like" table. This table must have the columns "id" and "ts" and "track_id" and "value" corresponding to an id, timestamp, counter track_id and associated counter value. | - -
- -
- -## Package: graphs - -### graphs.dominator_tree - -#### Macros - -
- -**graph_dominator_tree** . Given a table containing a directed flow-graph and an entry node, computes the "dominator tree" for the graph - -
- -
- -
- -Given a table containing a directed flow-graph and an entry node, computes -the "dominator tree" for the graph. See \[1\] for an explanation of what a -dominator tree is. - -\[1\] https://en.wikipedia.org/wiki/Dominator_(graph_theory) - -Example usage on traces containing heap graphs: -\`\`\` -CREATE PERFETTO VIEW dominator_compatible_heap_graph AS --- Extract the edges from the heap graph which correspond to references --- between objects. -SELECT -owner_id AS source_node_id, -owned_id as dest_node_id -FROM heap_graph_reference -JOIN heap_graph_object owner on heap_graph_reference.owner_id = owner.id -WHERE owned_id IS NOT NULL AND owner.reachable -UNION ALL --- Since a Java heap graph is a "forest" structure, we need to add a dummy --- "root" node which connects all the roots of the forest into a single --- connected component. -SELECT -(SELECT max(id) + 1 FROM heap_graph_object) as source_node_id, -id -FROM heap_graph_object -WHERE root_type IS NOT NULL; - -SELECT \* -FROM graph_dominator_tree!( -dominator_compatible_heap_graph, -(SELECT max(id) + 1 FROM heap_graph_object) -); -\`\`\` -Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, dominator_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|dominator_node_id\| is the id of the node in the input flow-graph which is the "dominator" of \|node_id\|. - -| Argument | Type | Description | -|---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed flow-graph on which the dominator tree should be computed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. Note: this means that the graph *must* be a single fully connected component with \|root_node_id\| (see below) being the "entry node" for this component. Specifically, all nodes *must* be reachable by following paths from the root node. Failing to adhere to this property will result in undefined behaviour. If working with a "forest"-like structure, a dummy node should be added which links all the roots of the forest together into a single component; an example of this can be found in the heap graph example query above. | -| root_node_id | Expr | The entry node to \|graph_table\| which will be the root of the dominator tree. | - -
- -
- -### graphs.partition - -#### Macros - -
- -**tree_structural_partition_by_group** . Partitions a tree into a forest of trees based on a given grouping key in a structure-preserving way. Specifically, for each tree in the output forest, all the nodes in that tree have the same ancestors and descendants as in the original tree *iff* that ancestor/descendent belonged to the same group. Example: Input id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 4 \| 1 Or as a graph: `1 (1) / 2 (1) / \ 3 (2) 4 (2) / \ 5 (1) 8 (1) / \ 6 (3) 7 (2)` Possible output (order of rows is implementation-defined) id \| parent_id \| group_key ---\|---\|--- 1 \| NULL \| 1 2 \| 1 \| 1 3 \| NULL \| 2 4 \| NULL \| 2 5 \| 2 \| 1 6 \| NULL \| 3 7 \| 4 \| 2 8 \| 2 \| 1 Or as a forest: `1 (1) 3 (2) 4 (2) 6 (3) | | 2 (1) 7 (2) / \ 5 (1) 8 (1)` - -
- -
- -
- -Partitions a tree into a forest of trees based on a given grouping key -in a structure-preserving way. - -Specifically, for each tree in the output forest, all the nodes in that tree -have the same ancestors and descendants as in the original tree *iff* that -ancestor/descendent belonged to the same group. - -Example: -Input - -| id | parent_id | group_key | -|---|---|---| -| 1 | NULL | 1 | -| 2 | 1 | 1 | -| 3 | NULL | 2 | -| 4 | NULL | 2 | -| 5 | 2 | 1 | -| 6 | NULL | 3 | -| 7 | 4 | 2 | -| 8 | 4 | 1 | - -Or as a graph: -`1 (1) -/ -2 (1) -/ \ -3 (2) 4 (2) -/ \ -5 (1) 8 (1) -/ \ -6 (3) 7 (2)` -Possible output (order of rows is implementation-defined) - -| id | parent_id | group_key | -|---|---|---| -| 1 | NULL | 1 | -| 2 | 1 | 1 | -| 3 | NULL | 2 | -| 4 | NULL | 2 | -| 5 | 2 | 1 | -| 6 | NULL | 3 | -| 7 | 4 | 2 | -| 8 | 2 | 1 | - -Or as a forest: -`1 (1) 3 (2) 4 (2) 6 (3) -| | -2 (1) 7 (2) -/ \ -5 (1) 8 (1)` -Returns: TableOrSubquery, The returned table has the schema (id UINT32, parent_id UINT32, group_key UINT32). - -| Argument | Type | Description | -|---|---|---| -| tree_table | TableOrSubquery | A table/view/subquery corresponding to a tree which should be partitioned. This table must have the columns "id", "parent_id" and "group_key". Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | - -
- -
- -### graphs.search - -#### Macros - -
- -**graph_reachable_dfs** . Computes the "reachable" set of nodes in a directed graph from a given set of starting nodes by performing a depth-first search on the graph - -
- -
- -
- -Computes the "reachable" set of nodes in a directed graph from a given set -of starting nodes by performing a depth-first search on the graph. The -returned nodes are structured as a tree with parent-child relationships -corresponding to the order in which nodes were encountered by the DFS. - -While this macro can be used directly by end users (hence being public), -it is primarily intended as a lower-level building block upon which higher -level functions/macros in the standard library can be built. - -Example usage on traces containing heap graphs: -`-- Compute the reachable nodes from the first heap root. -SELECT * -FROM graph_reachable_dfs!( -( -SELECT -owner_id AS source_node_id, -owned_id as dest_node_id -FROM heap_graph_reference -WHERE owned_id IS NOT NULL -), -(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) -);` -Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. - -| Argument | Type | Description | -|---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | -| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | - -
- -
- -
- -**graph_reachable_bfs** . Computes the "reachable" set of nodes in a directed graph from a given starting node by performing a breadth-first search on the graph - -
- -
- -
- -Computes the "reachable" set of nodes in a directed graph from a given -starting node by performing a breadth-first search on the graph. The returned -nodes are structured as a tree with parent-child relationships corresponding -to the order in which nodes were encountered by the BFS. - -While this macro can be used directly by end users (hence being public), -it is primarily intended as a lower-level building block upon which higher -level functions/macros in the standard library can be built. - -Example usage on traces containing heap graphs: -`-- Compute the reachable nodes from all heap roots. -SELECT * -FROM graph_reachable_bfs!( -( -SELECT -owner_id AS source_node_id, -owned_id as dest_node_id -FROM heap_graph_reference -WHERE owned_id IS NOT NULL -), -(SELECT id FROM heap_graph_object WHERE root_type IS NOT NULL) -);` -Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, parent_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a BFS search of the graph. - -| Argument | Type | Description | -|---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | -| start_nodes | TableOrSubquery | A table/view/subquery corresponding to the list of start nodes for the BFS. This table must have a single column "node_id". | - -
- -
- -
- -**graph_next_sibling** . Computes the next sibling node in a directed graph - -
- -
- -
- -Computes the next sibling node in a directed graph. The next node under a parent node -is determined by on the \|sort_key\|, which should be unique for every node under a parent. -The order of the next sibling is undefined if the \|sort_key\| is not unique. - -Example usage: -`-- Compute the next sibling: -SELECT * -FROM graph_next_sibling!( -( -SELECT -id AS node_id, -parent_id AS node_parent_id, -ts AS sort_key -FROM slice -) -);` -Returns: TableOrSubquery, The returned table has the schema (node_id UINT32, next_node_id UINT32). \|node_id\| is the id of the node from the input graph and \|next_node_id\| is the id of the node which is its next sibling. - -| Argument | Type | Description | -|---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph for which to find the next sibling. This table must have the columns "node_id", "node_parent_id" and "sort_key". | - -
- -
- -
- -**graph_reachable_weight_bounded_dfs** . Computes the "reachable" set of nodes in a directed graph from a set of starting (root) nodes by performing a depth-first search from each root node on the graph. The search is bounded by the sum of edge weights on the path and the root node specifies the max weight (inclusive) allowed before stopping the search. The returned nodes are structured as a tree with parent-child relationships corresponding to the order in which nodes were encountered by the DFS - -
- -
- -
- -Computes the "reachable" set of nodes in a directed graph from a set of -starting (root) nodes by performing a depth-first search from each root node on the graph. -The search is bounded by the sum of edge weights on the path and the root node specifies the -max weight (inclusive) allowed before stopping the search. -The returned nodes are structured as a tree with parent-child relationships corresponding -to the order in which nodes were encountered by the DFS. Each row also has the root node from -which where the edge was encountered. - -While this macro can be used directly by end users (hence being public), -it is primarily intended as a lower-level building block upon which higher -level functions/macros in the standard library can be built. - -Example usage on traces with sched info: -\`\`\` --- Compute the reachable nodes from a sched wakeup chain -INCLUDE PERFETTO MODULE sched.thread_executing_spans; - -SELECT \* -FROM -graph_reachable_dfs_bounded -!( -( -SELECT -id AS source_node_id, -COALESCE(parent_id, id) AS dest_node_id, -id - COALESCE(parent_id, id) AS edge_weight -FROM _wakeup_chain -), -( -SELECT -id AS root_node_id, -id - COALESCE(prev_id, id) AS root_target_weight -FROM _wakeup_chain -)); -\`\`\` -Returns: TableOrSubquery, The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32). \|root_node_id\| is the id of the starting node under which this edge was encountered. \|node_id\| is the id of the node from the input graph and \|parent_node_id\| is the id of the node which was the first encountered predecessor in a DFS search of the graph. - -| Argument | Type | Description | -|---|---|---| -| graph_table | TableOrSubquery | A table/view/subquery corresponding to a directed graph on which the reachability search should be performed. This table must have the columns "source_node_id" and "dest_node_id" corresponding to the two nodes on either end of the edges in the graph and an "edge_weight" corresponding to the weight of the edge between the node. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | -| root_table | TableOrSubquery | A table/view/subquery corresponding to start nodes to \|graph_table\| which will be the roots of the reachability trees. This table must have the columns "root_node_id" and "root_target_weight" corresponding to the starting node id and the max weight allowed on the tree. Note: the columns must contain uint32 similar to ids in trace processor tables (i.e. the values should be relatively dense and close to zero). The implementation makes assumptions on this for performance reasons and, if this criteria is not, can lead to enormous amounts of memory being allocated. | -| is_target_weight_floor | Expr | Whether the target_weight is a floor weight or ceiling weight. If it's floor, the search stops right after we exceed the target weight, and we include the node that pushed just passed the target. If ceiling, the search stops right before the target weight and the node that would have pushed us passed the target is not included. | - -
- -
- -## Package: export - -### export.to_firefox_profile - -#### Functions - -
- -**export_to_firefox_profile** -\> STRING. Dumps all trace data as a Firefox profile json string See `Profile` in https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js Also https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html You would probably want to download the generated json and then open at https://https://profiler.firefox.com You can easily do this from the UI via the following SQL `SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` The result will have a link for you to download this json as a file. - -
- -
- -
- -Dumps all trace data as a Firefox profile json string -See `Profile` in -https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js -Also -https://firefox-source-docs.mozilla.org/tools/profiler/code-overview.html - -You would probably want to download the generated json and then open at -https://https://profiler.firefox.com -You can easily do this from the UI via the following SQL -`SELECT CAST(export_to_firefox_profile() AS BLOB) AS profile;` -The result will have a link for you to download this json as a file. -Returns STRING: Json profile - -
- -
\ No newline at end of file From 2496da2b4aedbcb044cc85f86bb9195b6be22b2c Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Sun, 17 May 2026 14:55:34 +0000 Subject: [PATCH 21/61] Updates skills (2026-05-17 14:55) --- device-ai/appfunctions/SKILL.md | 7 +- identity/verified-email/SKILL.md | 420 ++++++++++++++++++ .../identity/credential-manager/index.md | 67 +++ .../credential-verifier.md | 166 +++++++ .../email-verification-implementation.md | 292 ++++++++++++ .../digital-credentials/email-verification.md | 145 ++++++ .../identity/digital-credentials/index.md | 96 ++++ .../identity/passkeys/create-passkeys.md | 325 ++++++++++++++ .../sign-in/credential-manager-webview.md | 84 ++++ performance/r8-analyzer/SKILL.md | 73 ++- .../r8-analyzer/references/CONFIGURATION.md | 6 +- 11 files changed, 1654 insertions(+), 27 deletions(-) create mode 100644 identity/verified-email/SKILL.md create mode 100644 identity/verified-email/references/android/identity/credential-manager/index.md create mode 100644 identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md create mode 100644 identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md create mode 100644 identity/verified-email/references/android/identity/digital-credentials/email-verification.md create mode 100644 identity/verified-email/references/android/identity/digital-credentials/index.md create mode 100644 identity/verified-email/references/android/identity/passkeys/create-passkeys.md create mode 100644 identity/verified-email/references/android/identity/sign-in/credential-manager-webview.md diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md index 50f6ef2..84ac2b2 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-05-14' + last-updated: '2026-05-16' keywords: - AppFunctions - Kotlin @@ -33,8 +33,9 @@ understand and use the provided functionality. ## Prerequisites -The app must **`targetSdk 36`** or newer as AppFunctions are part of the Android -platform API, are available from Android 16 onwards. +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, as it handles backwards compatibility. ## Workflows diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md new file mode 100644 index 0000000..6e4a9fa --- /dev/null +++ b/identity/verified-email/SKILL.md @@ -0,0 +1,420 @@ +--- +name: verified-email +description: Provides a complete workflow for implementing verified email retrieval + on Android Credential Manager API. Use this skill to integrate a secure, OTP-less + email verification flow into an Android app. This skill solves the problem of high-friction + sign-up processes by leveraging cryptographically verified credentials from trusted + providers like Google. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-16' + keywords: + - implementation + - Android + - Credential Manager + - Digital Credentials + - Verified Email + - OpenID4VP + - SD-JWT + - OTP-less + - authentication + - passkeys + - CredMan + - identity. +--- + +## Fundamentals + +- *[Overview of Digital Credentials](references/android/identity/digital-credentials/email-verification.md)*: Learn about cryptographically verifiable documents and the role of Credential Manager. +- *[Glossary](references/android/identity/digital-credentials/email-verification-implementation.md)* : Definitions for `dcql_query`, `UserInfoCredential`, and `GetDigitalCredentialOption`. + +### Standards \& Examples + +- *[OpenID4VP Standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction)*: The specification used to create digital credentials requests. +- *[Digital Credentials Demo](https://digital-credentials.dev/)*: Example requests and cross-platform testing tool. +- *[W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/)*: The data model for cryptographically secured claims. +- *[SD-JWT](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/)*: Selective Disclosure JSON Web Token format used for responses. +- *[mdoc](https://www.iso.org/standard/69084.html)*: ISO/IEC 18013-5 standard for mobile documents. + +### Requirements + +- **SDK Version**: Minimum SDK 28 (Android 9) is required. +- **GMS Version**: Google Play services version 25.49.x or higher. + +### Use Cases + +Email verification is applicable for the following use cases: + +- **Account Creation/Sign-up**: Remove friction by skipping manual email verification. +- **Account Recovery**: Securely verify email ownership during recovery flows. +- **Re-authentication**: Versatile verification for high-risk actions, independent of the initial sign-in method. + +### Limitations \& Nuances + +- **Workspace Accounts**: Google does not issue verifiable credentials for Google Workspace Accounts. +- **Freshness**: For non-@gmail.com addresses, Google verifies the email at account creation but there is no freshness claim; implement an additional challenge like an OTP. + +### 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. + +## 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: + +- `SignUpScreen` +- `"Email address"` +- `"Recover Account"` +- `"Account Recovery"` +- `"Forgot password?"` +- `"Delete Account"` + +## 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: + +### 1. Search for Navigation Routes + +If your app uses Navigation, search for routes or destinations related +to authentication: + +Look for: + +- **Keywords** : `signup`, `registration`, `create_account`, `forgot_password`, `recovery`, `verify_email`. +- **Code Pattern** : Search for `NavHost` or `composable` destinations using these strings. + +### 2. Locate Authentication ViewModels + +Find the business logic handling user attributes and account creation, account +recovery: + +- **Keywords** : `SignUpViewModel`, `AuthViewModel`, `RegistrationRepository`. +- **Code Pattern** : Look for methods like `onCrea teAccount`, `onRecoverAccount`, or `validateEmail`. + +### 3. Find instances of reauthentication for sensitive actions + +For reauthentication use cases, find areas where users perform sensitive +actions: + +- **Keywords** : `ChangePassword`, `UpdatePayment`, `DeleteAccount`, `UpdateDetails`, `EditUserDetails` + +## Important pointers for Implementation + +- Construct a Digital Credential Request and present it to the user. +- Make sure to follow the request JSON structure as mentioned in [documentation](references/android/identity/digital-credentials/email-verification-implementation.md). +- While presenting the request to the user, check if result credential is DigitalCredential and credential.credentialJson as responseJsonString +- Parse the response from the client. +- Offer a passkey creation option if one is not already present. +- Assume a local `SdJwtParser` to parse raw SD-JWT and return a `JSONObject`. +- Use a `VerifiedUserInfo` data class to store the parsed name and email. +- Leave a TODO for developers to handle the app's server-side validation and parsing. +- Direct users to the home screen after API call success and show a snackbar with user details for reference purpose only. + +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: + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.credentials:credentials:1.7.0-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" +} +``` + +## Initialize 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) + +## 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. + +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() + + // This request follows the OpenID4VP spec + val openId4vpRequest = """ + { + "requests": [ + { + "protocol": "openid4vp-v1-unsigned", + "data": { + "response_type": "vp_token", + "response_mode": "dc_api", + "nonce": "$nonce", + "dcql_query": { + "credentials": [ + { + "id": "user_info_query", + "format": "dc+sd-jwt", + "meta": { + "vct_values": ["UserInfoCredential"] + }, + "claims": [ + {"path": ["email"]}, + {"path": ["name"]}, + {"path": ["given_name"]}, + {"path": ["family_name"]}, + {"path": ["picture"]}, + {"path": ["hd"]}, + {"path": ["email_verified"]} + ] + } + ] + } + } + } + ] + } + """ + + val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest) + val request = GetCredentialRequest(listOf(getDigitalCredentialOption)) + +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: + + - `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. Currently, 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. + +- `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()`. + +## Present the request to the user + +Present the user with the request, using the Credential Manager built-in UI. + + try { + // Requesting Digital Credential from user... + val result = credentialManager.getCredential(activity, request) + + when (val credential = result.credential) { + is DigitalCredential -> { + val responseJsonString = credential.credentialJson + + // Successfully received digital credential response. + + // Next, parse this response and send it to your server. + // ... + } + + else -> { + // handle Unexpected State() - Up to the developer + } + } + } catch (e: Exception) { + // handle exceptions - Up to the developer + } + +> [!NOTE] +> **Note:** There is no equivalent of Sign in with Google's `preferImmediatelyAvailableCredentials` for Digital Credentials. If no verifiable credential is found (for example, no eligible account on device), the user will be shown a "No options available" or similar system screen. + +## 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. + +> [!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. + + // 1. Parse the outer JSON wrapper to get the `vp_token` + val responseData = JSONObject(responseJsonString) + val vpToken = responseData.getJSONObject("vp_token") + + // 2. Extract the raw SD-JWT string + val credentialId = vpToken.keys().next() + val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0) + + // 3. Use your parser to get the verified claims + // Server-side validation/parsing is highly recommended. + + // Assumes a local parser like the one in our SdJwtParser.kt sample + val claims = SdJwtParser.parse(rawSdJwt) + Log.d("TAG", "Parsed Claims: ${claims.toString(2)}") + + // 4. Create your VerifiedUserInfo object with REAL data + val userInfo = VerifiedUserInfo( + email = claims.getString("email"), + displayName = claims.optString("name", claims.getString("email")) + ) + +## Handle the 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: + + /* + // 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..." + ] + } + } + + // Example of the parsed and verified claims from the SD-JWT on your server: + { + "cnf": { + "jwk": {..} + }, + "exp": 1775688222, + "iat": 1775083422, + "iss": "https://verifiablecredentials-pa.googleapis.com", + "vct": "UserInfoCredential", + "email": "jane.doe.246745@gmail.com", + "email_verified": true, + "given_name": "Jane", + "family_name": "Doe", + "name": "Jane Doe", + "picture": "http://example.com/janedoe/me.jpg", + "hd": "" + } + */ + +> [!NOTE] +> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). + +## 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. + +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: + +- **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. + +The validation on the server must achieve the following: + +- **Verify issuer** : Ensure the `iss` (issuer) field matches `https://verifiablecredentials-pa.googleapis.com`. +- **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). + +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. + + try { + // Send the raw credential response and the original nonce to your server. + // Your server must validate the response. createAccountWithVerifiedCredentials + // is a custom implementation per each RP for server side verification and account creation. + val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce) + + // Server returns the new account info (e.g., email, name) + val claims = JSONObject(serverResponse.json) + + val userInfo = VerifiedUserInfo( + email = claims.getString("email"), + displayName = claims.optString("name", claims.getString("email")) + ) + + // handle response - Up to the developer + } catch (e: Exception) { + // handle exceptions - Up to the developer + } + +## 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. + +## WebView support + +For the flow to work on a 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 to signal the native app, which can then perform the actual call +to the Credential Manager API. + +## See also + +- [Overview of verified email retrieval](references/android/identity/digital-credentials/email-verification.md) +- [Credential Manager](references/android/identity/credential-manager/index.md) + +## Critical Security Guidelines + +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. +- **Cryptographic Checks** : The app's server must validate the issuer (`iss`) field, the SD-JWT signature, and the presenter identity using the `cnf` field. diff --git a/identity/verified-email/references/android/identity/credential-manager/index.md b/identity/verified-email/references/android/identity/credential-manager/index.md new file mode 100644 index 0000000..c646f19 --- /dev/null +++ b/identity/verified-email/references/android/identity/credential-manager/index.md @@ -0,0 +1,67 @@ +[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: + +- **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. +- **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). +- **Autofill integration** : Integrates with [autofill](https://developer.android.com/identity/autofill/credential-manager-autofill) to display credentials within the autofill UI. + +## Benefits of using Credential Manager + +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. +- **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: + +![The Credential Manager user interface for passkeys, passwords, and federated sign-ins](https://developer.android.com/static/identity/credential-manager/images/credman-ui.png) **Figure 1.**Credential Manager's built-in unified interface for passkeys, passwords, and federated sign-ins + +## Authentication terminology + +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. +- **Credential provider** : A component that stores and provides user credentials, such as Google Password Manager. Note that the [FIDO documentation](https://fidoalliance.org/passkeys/) refers to credential providers as credential managers. + +## 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). + +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: + +- [Google Sign-In](https://developer.android.com/identity/sign-in/legacy-gsi-migration) +- [One Tap](https://developer.android.com/identity/legacy/one-tap) +- [Smart Lock for Passwords](https://developer.android.com/identity/sign-in/smart-lock-migration) +- [FIDO2](https://developer.android.com/identity/sign-in/fido2-migration) \ No newline at end of file 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 new file mode 100644 index 0000000..3f7f9c9 --- /dev/null +++ b/identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md @@ -0,0 +1,166 @@ +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. + +### Android version compatibility + +The Verifier API is supported on Android 9 (API level 28) and higher. + +### Implementation + +To verify digital credentials in your Android project, do the following: + +1. Add dependencies to your app's build script and initialize a `CredentialManager` class. +2. Construct a digital credential request and use it to initialize a `DigitalCredentialOption`, followed by building the `GetCredentialRequest`. +3. Launch the `getCredential` flow with the constructed request to receive a successful `GetCredentialResponse` or handle any exceptions that may occur. Upon successful retrieval, validate the response. + +#### Add dependencies and initialize + +Add the following dependencies to your Gradle build script: + + dependencies { + implementation("androidx.credentials:credentials:1.6.0-beta01") + implementation("androidx.credentials:credentials-play-services-auth:1.6.0-beta01") + } + +Next, Initialize an instance of the `CredentialManager` class. + + val credentialManager = CredentialManager.create(context) + +#### Construct a digital credential request + +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. + val requestJson = generateRequestFromServer() + val digitalCredentialOption = + GetDigitalCredentialOption(requestJson = requestJson) + + // Use the option from the previous step to build the `GetCredentialRequest`. + val getCredRequest = GetCredentialRequest( + 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). + + { + "requests": [ + { + "protocol": "openid4vp-v1-unsigned", + "data": { + "response_type": "vp_token", + "response_mode": "dc_api", + "nonce": "OD8eP8BYfr0zyhgq4QCVEGN3m7C1Ht_No9H5fG5KJFk", + "dcql_query": { + "credentials": [ + { + "id": "cred1", + "format": "mso_mdoc", + "meta": { + "doctype_value": "org.iso.18013.5.1.mDL" + }, + "claims": [ + { + "path": [ + "org.iso.18013.5.1", + "family_name" + ] + }, + { + "path": [ + "org.iso.18013.5.1", + "given_name" + ] + }, + { + "path": [ + "org.iso.18013.5.1", + "age_over_21" + ] + } + ] + } + ] + } + } + } + ] + } + +#### 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. + +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 { + val result = credentialManager.getCredential( + context = activityContext, + request = getCredRequest + ) + verifyResult(result) + } catch (e : GetCredentialException) { + handleFailure(e) + } + } + + // Handle the successfully returned credential. + fun verifyResult(result: GetCredentialResponse) { + val credential = result.credential + when (credential) { + is DigitalCredential -> { + val responseJson = credential.credentialJson + validateResponseOnServer(responseJson) + } + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential ${credential.type}") + } + } + } + + // Handle failure. + fun handleFailure(e: GetCredentialException) { + when (e) { + is GetCredentialCancellationException -> { + // The user intentionally canceled the operation and chose not + // to share the credential. + } + is GetCredentialInterruptedException -> { + // Retry-able error. Consider retrying the call. + } + is NoCredentialException -> { + // No credential was available. + } + is CreateCredentialUnknownException -> { + // An unknown, usually unexpected, error has occurred. Check the + // message error for any additional debugging information. + } + is CreateCredentialCustomException -> { + // You have encountered a custom error thrown by the wallet. + // If you made 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}") + } + } \ No newline at end of file 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 new file mode 100644 index 0000000..e1d5b32 --- /dev/null +++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md @@ -0,0 +1,292 @@ +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: + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.credentials:credentials:1.7.0-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" +} +``` + +## Initialize 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) + +## 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. + +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() + + // This request follows the OpenID4VP spec + val openId4vpRequest = """ + { + "requests": [ + { + "protocol": "openid4vp-v1-unsigned", + "data": { + "response_type": "vp_token", + "response_mode": "dc_api", + "nonce": "$nonce", + "dcql_query": { + "credentials": [ + { + "id": "user_info_query", + "format": "dc+sd-jwt", + "meta": { + "vct_values": ["UserInfoCredential"] + }, + "claims": [ + {"path": ["email"]}, + {"path": ["name"]}, + {"path": ["given_name"]}, + {"path": ["family_name"]}, + {"path": ["picture"]}, + {"path": ["hd"]}, + {"path": ["email_verified"]} + ] + } + ] + } + } + } + ] + } + """ + + val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest) + val request = GetCredentialRequest(listOf(getDigitalCredentialOption)) + +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: + + - `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. Currently, 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. + +- `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()`. + +## Present the request to the user + +Present the user with the request, using the Credential Manager built-in UI. + + try { + // Requesting Digital Credential from user... + val result = credentialManager.getCredential(activity, request) + + when (val credential = result.credential) { + is DigitalCredential -> { + val responseJsonString = credential.credentialJson + + // Successfully received digital credential response. + + // Next, parse this response and send it to your server. + // ... + } + + else -> { + // handle Unexpected State() - Up to the developer + } + } + } catch (e: Exception) { + // handle exceptions - Up to the developer + } + +> [!NOTE] +> **Note:** There is no equivalent of Sign in with Google's `preferImmediatelyAvailableCredentials` for Digital Credentials. If no verifiable credential is found (for example, no eligible account on device), the user will be shown a "No options available" or similar system screen. + +## 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. + +> [!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. + + // 1. Parse the outer JSON wrapper to get the `vp_token` + val responseData = JSONObject(responseJsonString) + val vpToken = responseData.getJSONObject("vp_token") + + // 2. Extract the raw SD-JWT string + val credentialId = vpToken.keys().next() + val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0) + + // 3. Use your parser to get the verified claims + // Server-side validation/parsing is highly recommended. + + // Assumes a local parser like the one in our SdJwtParser.kt sample + val claims = SdJwtParser.parse(rawSdJwt) + Log.d("TAG", "Parsed Claims: ${claims.toString(2)}") + + // 4. Create your VerifiedUserInfo object with REAL data + val userInfo = VerifiedUserInfo( + email = claims.getString("email"), + displayName = claims.optString("name", claims.getString("email")) + ) + +## Handle the 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: + + /* + // 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..." + ] + } + } + + // Example of the parsed and verified claims from the SD-JWT on your server: + { + "cnf": { + "jwk": {..} + }, + "exp": 1775688222, + "iat": 1775083422, + "iss": "https://verifiablecredentials-pa.googleapis.com", + "vct": "UserInfoCredential", + "email": "jane.doe.246745@gmail.com", + "email_verified": true, + "given_name": "Jane", + "family_name": "Doe", + "name": "Jane Doe", + "picture": "http://example.com/janedoe/me.jpg", + "hd": "" + } + */ + +> [!NOTE] +> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). + +## 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. + +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: + +- **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. + +The validation on the server must achieve the following: + +- **Verify issuer** : Ensure the `iss` (issuer) field matches `https://verifiablecredentials-pa.googleapis.com`. +- **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). + +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. + + try { + // Send the raw credential response and the original nonce to your server. + // Your server must validate the response. createAccountWithVerifiedCredentials + // is a custom implementation per each RP for server side verification and account creation. + val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce) + + // Server returns the new account info (e.g., email, name) + val claims = JSONObject(serverResponse.json) + + val userInfo = VerifiedUserInfo( + email = claims.getString("email"), + displayName = claims.optString("name", claims.getString("email")) + ) + + // handle response - Up to the developer + } catch (e: Exception) { + // handle exceptions - Up to the developer + } + +## 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. + +## WebView support + +For the flow to work on a 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 to signal the native app, which can then perform the actual call +to the Credential Manager API. + +## See also + +- [Overview of verified email retrieval](https://developer.android.com/identity/digital-credentials/email-verification) +- [Credential Manager](https://developer.android.com/identity/credential-manager) \ No newline at end of file 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 new file mode 100644 index 0000000..ac696ee --- /dev/null +++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md @@ -0,0 +1,145 @@ +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: + +- Android compatibility +- User experience +- Accounts supported +- Implication of email deliverability +- Comparison with [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) + +This guide assumes you are familiar with the following concepts: + +- [Credential Manager](https://developer.android.com/identity/credential-manager) +- [Digital Credentials](https://developer.android.com/identity/digital-credentials) +- [Verifiable Credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) + +## 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. + +## 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 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. + +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. + + > [!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. + + > [!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: + +- **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. + +#### 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. +![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. +![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. +![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. + +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). + +> [!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. +> For phone number verification, see [Firebase Phone Number Verification](https://firebase.google.com/docs/phone-number-verification). Note that you can't request a phone number and email in the same invocation as these are separate capabilities. + +## 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. + +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. + +### 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: + +- **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. +- **Platform support**: The Credential Manager flow is an Android-only solution. +- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It cannot be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes). + +## Next steps + +To implement this feature in your app, see the [Implementation guide](https://developer.android.com/identity/digital-credentials/email-verification-implementation). \ No newline at end of file diff --git a/identity/verified-email/references/android/identity/digital-credentials/index.md b/identity/verified-email/references/android/identity/digital-credentials/index.md new file mode 100644 index 0000000..5f9ca67 --- /dev/null +++ b/identity/verified-email/references/android/identity/digital-credentials/index.md @@ -0,0 +1,96 @@ +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: +![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. + +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: + +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. +3. When the user selects a credential in the list, Credential Manager then invokes the wallet, which will handle the remainder of the transaction (showing UIs, etc.) and return the credential to the application. + +This flow is shown here: +![Image showing the flow of a digital credential interaction](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_flowchart.svg) **Figure 2.** Interaction model for digital credential verification. Credential Manager uses pre-registered credentials metadata across user wallet(s) to match a verifier's request and prompts the user to select a credential. Credential Manager then directs the activity flow to the corresponding wallet which handles the remainder of the transaction and returns the credential to the verifier. Note: The verifier needs to handle and verify the credential response once it is returned. + +## 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. + +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. + +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: +![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 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. + +### Try it out + +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. +3. Navigate to and select `Request Credentials (OpenID4VP)`. +4. Accept the warning prompts and scan the QR Code with your phone, then select "Use passkey" and tap through the confirmation to show the available credentials. +5. Select the credential from CMWallet to return to the browser. The browser should show the returned credential. + +### See also + +- To learn more about using Credential Manager to request digital credentials in your app, read the [Credential Manager - Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) page. +- To learn more about building a digital wallet using Credential Manager, read the [Credential Manager - Holder API](https://developer.android.com/identity/digital-credentials/credential-holder) page. \ No newline at end of file diff --git a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md new file mode 100644 index 0000000..1ae9341 --- /dev/null +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -0,0 +1,325 @@ +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-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" +} +``` + +> [!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/verified-email/references/android/identity/sign-in/credential-manager-webview.md b/identity/verified-email/references/android/identity/sign-in/credential-manager-webview.md new file mode 100644 index 0000000..a953795 --- /dev/null +++ b/identity/verified-email/references/android/identity/sign-in/credential-manager-webview.md @@ -0,0 +1,84 @@ +This document describes how to integrate the Credential Manager API with an +Android app that uses WebView. Credential Manager is supported natively in the +`android.webkit.WebView` library in [version 1.12.0](https://developer.android.com/jetpack/androidx/releases/webkit#1.12.0) and later. + +## Prerequisites + +To use Credential Manager in WebView, add the following dependencies to your app +module's build script: + + dependencies { + implementation("androidx.credentials:credentials:1.6.0-beta02") + implementation("androidx.credentials:credentials-play-services-auth:1.6.0-beta02") + implementation("androidx.webkit:webkit:1.14.0") + } + +You will also need to associate your app with a website that your app owns using +digital asset linking. For more information, see +[adding digital asset linking](https://developer.android.com/identity/credential-manager/prerequisites). + +## Use the WebKit library + +To use the WebKit library, check for feature support, and then enable support by +calling `setWebAuthenticationSupport()`: + + class WebViewActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + // Put your webview link here. + val url = "https://project-sesame-426206.appspot.com/passkey-signup" + AndroidView( + factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + + webViewClient = WebViewClientImpl() + } + }, + update = { webView -> + run { + webView.loadUrl(url) + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_AUTHENTICATION)) { + WebSettingsCompat.setWebAuthenticationSupport( + webView.settings, + WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_FOR_APP, + ) + // Check if getWebauthenticationSupport may have been disabled by the WebView. + Log.e( + "WebViewPasskeyDemo", + "getWebAuthenticationSupport result: " + WebSettingsCompat.getWebAuthenticationSupport( + webView.settings + ), + ) + } else { + Log.e("WebViewPasskeyDemo", "WebView does not support passkeys.") + } + } + }, + ) + } + } + } + +> [!NOTE] +> **Note:** The WebKit library doesn't support `mediation:"conditional"` requests. + +## Web integration + +To learn how to build Web integration see +[Create a passkey for passwordless logins](https://web.dev/passkey-registration/). +You can also reference the [demo site source](https://github.com/deephand/webauthn-in-webview). + +## Testing and deployment + +Test the entire flow thoroughly in a controlled environment to verify proper +communication between the Android app, the web page, and the backend. + +Deploy the integrated solution to production, verifying that the backend can +handle incoming registration and authentication requests. The backend code +should generate initial JSON for registration (create) and authentication (get) +processes. It should also handle validation and verification of the responses +received from the web page. + +Verify the implementation corresponds to the [UX recommendations](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys). \ No newline at end of file diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index a661bc0..c384a83 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-04-25' + last-updated: '2026-05-16' keywords: - R8 - proguard @@ -16,26 +16,57 @@ metadata: - optimization --- -## Core workflow +## Step 1. Setup and Configuration Check -- \[ \] Step 1: Create a file called R8_Configuration_Analysis.md, or reuse if one exists already, to store the output -- \[ \] Step 2: Look at the configuration of R8 by looking at build.gradle, build.gradle.kts, gradle.properties in the codebase using [references/CONFIGURATION.md](references/CONFIGURATION.md) as the reference. Inform the developer and add the analysis to the report file -- \[ \] Step 3: If the AGP version is less than 9, suggest moving to AGP 9.0 version as AGP 9.0 includes [optimizations](references/android/topic/performance/app-optimization/enable-app-optimization.md). - - \[ \] Step 4: Look at the proguard files in the codebase and evaluate each keep rule in the following specific order: a. **Libraries check** : Check rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). If the app has keep rules targeting libraries - Google, AndroidX, Kotlin, Kotlinx, Room, Gson, Retrofit, inform the user that these are not required and suggest removal of these rules. b. **Impact analysis** : For the remaining keep rules, assess them based on the impact hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md). (Note: Do NOT assess the impact of keep rules already covered in the libraries check step). -- \[ \] Step 5: Identify subsuming keep rules in the remaining keep rules based on the hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and suggest removing the broader keep rules. -- \[ \] Step 6: For each remaining keep rule, analyze in detail the code affected by the rule by examining the code and adjacent files to understand why it was written. Look for reflection usage in those packages, and suggest a narrow and specific keep rule for the scenario using [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md). -- \[ \] Step 7: For every keep rule inform concisely and to the point what action needs to be taken - whether the rule needs to be removed/refined. - - If refining the rule, give instructions on finding a narrower and specific keep rule using the [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md). - - If removing, provide reasoning on why it needs to be removed. -- \[ \] Step 8: After keep analysis, order the keep rule analysis based on the impact to the codebase hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) -- \[ \] Step 9: Advise the user to run tests using [UI automator](references/android/training/testing/other-components/ui-automator.md) to assess that there is no issue with the suggested changes, concentrating on the packages where keep rules will be affected. +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. +- Use [/references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. +- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement + performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) +- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. -## Mandatory rules +## Step 2. Analysis Path Selection -- Don't make any changes in keep rule files -- Don't say about what level each keep rule is. -- Don't generate parts of the report if there is no keep rule to report in that section. -- Don't mention the generated files. -- Don't mention exceptions that occur during execution. -- Don't mention the benefits of R8 -- Don't mention any files of this skill +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` + and `libs.versions.toml` to + get the R8 version + +- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. + +- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. + +### Path A: Quantitative Data Generation (R8 \>= 9.3.7-dev) + +- **Check Requirements** : Python and `protobuf` package are mandatory. +- **Generate and Analyze** : You MUST run the shell commands described in `[/references/CONFIGURATION-ANALYZER.md][7]` to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. +- **Report**: Rely entirely on the generated file analysis.txt for scores and rule impact metrics. Proceed to Step 3. + +### Path B: Heuristic Evaluation and Recommendation (R8 \< 9.3.7-dev) + +*(Use ONLY if quantitative data generation is not possible)* + +- **Manual Evaluation** : Inspect `proguard-rules.pro`. +- **Library Check** : Compare rules against [/references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md) .Suggest **Remove** for bundled rules. +- **Custom Rule Check** : Use [/references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (e.g., package-wide). +- **Validation** : Suggest Macrobenchmark tests using [ui automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. + +## Step 3. Report Generation + +- **Format** : Follow `[/references/REPORT_FORMAT.md][8]` strictly. +- **Input**: Extract metrics (Scores, Impacts, Example Classes) + directly from generated file analysis.txt if using Path A, + or from manual findings if using Path B. + +- **Output** : + Output ONLY the raw Markdown report in the chat. + Do NOT output conversational filler (e.g., "Here is your report..."). + Do NOT provide recommendations, next steps, + or any other text outside of the sections defined in + `[/references/REPORT_FORMAT.md][8]` + Do NOT mention the path used for analysis of the configuration + +## Constraints + +- **Strict Output Limit**: The final output MUST strictly be the Markdown report and nothing else. +- **No Code Changes**: Research and suggest only; Do not modify files. +- **No Redundancy**: Do not explain R8 benefits or reference skill internal files in the report. +- **Focus**: Omit sections (e.g., Subsumed Rules, Configuration) if no issues or items are found. diff --git a/performance/r8-analyzer/references/CONFIGURATION.md b/performance/r8-analyzer/references/CONFIGURATION.md index 24e1301..ae04947 100644 --- a/performance/r8-analyzer/references/CONFIGURATION.md +++ b/performance/r8-analyzer/references/CONFIGURATION.md @@ -1,9 +1,9 @@ To achieve maximum utilization of R8, the codebase must be configured correctly -depending on the build script language (Kotlin DSL vs. Groovy DSL). +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 should enable minification +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`). @@ -36,7 +36,7 @@ type for release and performance testing. It MUST use the optimized default file **Full Mode:** R8 Full Mode enables the entire optimizations - **AGP 8.0+** : Enabled by default. Ensure `android.enableR8.fullMode=false` is **NOT** present. -- **Pre-AGP 8.0** : Should be explicitly enabled with `android.enableR8.fullMode=true`. +- **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: From 93c3d0a360a12d8eabc16fb25873d227f1665836 Mon Sep 17 00:00:00 2001 From: Vinisha Athwani <35296563+vinishavathwani@users.noreply.github.com> Date: Mon, 18 May 2026 20:56:23 +0530 Subject: [PATCH 22/61] Update R8 skill (#44) * Update links, minor fixes for R8 skill * Minor clean ups * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update SKILL.md --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- performance/r8-analyzer/SKILL.md | 41 ++- .../references/CONFIGURATION-ANALYZER.md | 301 ++++++++++++++++++ .../r8-analyzer/references/REPORT_FORMAT.md | 57 ++++ 3 files changed, 378 insertions(+), 21 deletions(-) create mode 100644 performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md create mode 100644 performance/r8-analyzer/references/REPORT_FORMAT.md diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index c384a83..51dc623 100644 --- a/performance/r8-analyzer/SKILL.md +++ b/performance/r8-analyzer/SKILL.md @@ -16,15 +16,15 @@ metadata: - optimization --- -## Step 1. Setup and Configuration Check +## Step 1. Setup and configuration check - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. -- Use [/references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. +- Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. - **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) -- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. +- **Full mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. -## Step 2. Analysis Path Selection +## Step 2. Analysis path selection - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and `libs.versions.toml` to @@ -34,39 +34,38 @@ metadata: - **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. -### Path A: Quantitative Data Generation (R8 \>= 9.3.7-dev) +### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) -- **Check Requirements** : Python and `protobuf` package are mandatory. -- **Generate and Analyze** : You MUST run the shell commands described in `[/references/CONFIGURATION-ANALYZER.md][7]` to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. +- **Check requirements** : Python and `protobuf` package are mandatory. +- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. - **Report**: Rely entirely on the generated file analysis.txt for scores and rule impact metrics. Proceed to Step 3. -### Path B: Heuristic Evaluation and Recommendation (R8 \< 9.3.7-dev) +### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) *(Use ONLY if quantitative data generation is not possible)* -- **Manual Evaluation** : Inspect `proguard-rules.pro`. -- **Library Check** : Compare rules against [/references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md) .Suggest **Remove** for bundled rules. -- **Custom Rule Check** : Use [/references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (e.g., package-wide). -- **Validation** : Suggest Macrobenchmark tests using [ui automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. +- **Manual evaluation** : Inspect `proguard-rules.pro`. +- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. +- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). +- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. -## Step 3. Report Generation +## Step 3. Report generation -- **Format** : Follow `[/references/REPORT_FORMAT.md][8]` strictly. +- **Format** : Follow [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) strictly. - **Input**: Extract metrics (Scores, Impacts, Example Classes) directly from generated file analysis.txt if using Path A, or from manual findings if using Path B. - - **Output** : Output ONLY the raw Markdown report in the chat. - Do NOT output conversational filler (e.g., "Here is your report..."). + Do NOT output conversational filler (for example, "Here is your report..."). Do NOT provide recommendations, next steps, or any other text outside of the sections defined in - `[/references/REPORT_FORMAT.md][8]` + [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) Do NOT mention the path used for analysis of the configuration ## Constraints -- **Strict Output Limit**: The final output MUST strictly be the Markdown report and nothing else. -- **No Code Changes**: Research and suggest only; Do not modify files. -- **No Redundancy**: Do not explain R8 benefits or reference skill internal files in the report. -- **Focus**: Omit sections (e.g., Subsumed Rules, Configuration) if no issues or items are found. +- **Strict output limit**: The final output MUST strictly be the Markdown report and nothing else. +- **No code changes**: Research and suggest only; Do not modify files. +- **No redundancy**: Do not explain R8 benefits or reference skill internal files in the report. +- **Focus**: Omit sections (for example, Subsumed Rules, Configuration) if no issues or items are found. \ No newline at end of file diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md new file mode 100644 index 0000000..6f1ca6f --- /dev/null +++ b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -0,0 +1,301 @@ +# Configuration Analyzer data generation + +On each step, keep the user informed of the progress by displaying the output. + +### 1. Requirements + +- R8 Version: 9.3.7-dev or later + +### 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: + +```bash +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: + +```bash +# Remove the intermediate JSON and the directory containing protobuf files +rm tmp/r8analysis/keepruleradius.json +rm tmp/r8analysis/*.pb + +# Copy the previous result to history before deleting the analysis +if [ -f tmp/r8analysis/analysis_result.txt ]; +then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && + rm tmp/r8analysis/analysis_result.txt; fi + + +``` +### 4. 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 + +```bash +./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. + +```python +import sys +import os +import glob +from google.protobuf import json_format +import keep_radius_pb2 + +def convert_pb_to_json(input_pb_path, output_json_path): + bundle = keep_radius_pb2.BlastRadiusContainer() + + try: + with open(input_pb_path, "rb") as pb_file: + binary_data = pb_file.read() + except Exception as e: + print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) + return False + + try: + bundle.ParseFromString(binary_data) + except Exception as e: + print(f"Error parsing protobuf: {e}", file=sys.stderr) + return False + + try: + json_string = json_format.MessageToJson( + bundle, + always_print_fields_with_no_presence=True, + preserving_proto_field_name=True, + indent=4 + ) + with open(output_json_path, "w", encoding="utf-8") as json_file: + json_file.write(json_string) + return True + except Exception as e: + print(f"Error writing JSON: {e}", file=sys.stderr) + return False + +if __name__ == "__main__": + input_pb = sys.argv[1] if len(sys.argv) > 1 else None + if not input_pb: + pb_files = glob.glob("tmp/r8analysis/*.pb") + if not pb_files: + print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) + sys.exit(1) + input_pb = sorted(pb_files)[-1] # Use the most recent one + output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" + if not convert_pb_to_json(input_pb, output_json): + sys.exit(1) +``` + +### 6. Analyze + +Run the following analysis script on the generated JSON to get the impact of +the keep rules and sort it. + +```python +import json, sys + +def analyze(path): + try: + with open(path, 'r') as f: + d = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Build reference map + c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} + r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} + + tot_opt = tot_obf = tot_shr = tot_items = 0 + + # Tally constraints across all kept items + for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): + for i in d.get(tbl, []): + tot_items += 1 + kb = i.get('kept_by', []) + if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 + if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 + if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 + + # Find denominator + bi = d.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else tot_items + + # Check for globals + globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] + def score(cnt, flag): + if any(flag in src for src in globals_src): return 0.0 + return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) + + result = [ + f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", + f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", + f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" + ] + for line in result: + print(line) + with open("tmp/r8analysis/analysis_result.txt", "w") as f: + f.write("\n".join(result)) + +if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" + analyze(path) +``` +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. + +```python +import json, sys + +def report(path): + try: + with open(path, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Calculate denominator for percentage + bi = data.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) + + processed = [] + for r in data.get('keep_rule_blast_radius_table', []): + br = r.get('blast_radius', {}) + c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) + impact = c + f + m + if impact == 0: + continue + impact_pct = (impact / denom * 100) if denom > 0 else 0.0 + processed.append({ + 'id': r.get('id'), + 'source': r.get('source'), + 'impact': impact, + 'impact_pct': f"{impact_pct:.2f}%", + 'classes': c, + 'fields': f, + 'methods': m, + 'subsumed_by': br.get('subsumed_by', []) + }) + + processed.sort(key=lambda x: x['impact'], reverse=True) + + # Output JSON for the agent to fetch and process + print(json.dumps({ + "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], + "subsumed": [r for r in processed if r['subsumed_by']] + }, indent=2)) + +if __name__ == "__main__": + report("tmp/r8analysis/keepruleradius.json") +``` +Add this data to the `analysis_result.txt` with the top impactful rules and +subsumed rules. + +### 8. Compare with previous report + +If `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 "tmp/r8analysis" folder + +```bash +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. + +```python +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 4, + '', + 'keep_radius.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' + _globals['_KEEPRULETAG']._serialized_start = 3332 + _globals['_KEEPRULETAG']._serialized_end = 3363 + _globals['_KEEPCONSTRAINT']._serialized_start = 3365 + _globals['_KEEPCONSTRAINT']._serialized_end = 3437 + _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 + _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 + _globals['_BLASTRADIUS']._serialized_start = 351 + _globals['_BLASTRADIUS']._serialized_end = 470 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 + _globals['_KEEPCONSTRAINTS']._serialized_start = 601 + _globals['_KEEPCONSTRAINTS']._serialized_end = 707 + _globals['_KEPTCLASSINFO']._serialized_start = 709 + _globals['_KEPTCLASSINFO']._serialized_end = 805 + _globals['_KEPTFIELDINFO']._serialized_start = 807 + _globals['_KEPTFIELDINFO']._serialized_end = 903 + _globals['_KEPTMETHODINFO']._serialized_start = 905 + _globals['_KEPTMETHODINFO']._serialized_end = 1003 + _globals['_FIELDREFERENCE']._serialized_start = 1005 + _globals['_FIELDREFERENCE']._serialized_end = 1102 + _globals['_METHODREFERENCE']._serialized_start = 1104 + _globals['_METHODREFERENCE']._serialized_end = 1203 + _globals['_PROTOREFERENCE']._serialized_start = 1205 + _globals['_PROTOREFERENCE']._serialized_end = 1280 + _globals['_TYPEREFERENCE']._serialized_start = 1282 + _globals['_TYPEREFERENCE']._serialized_end = 1334 + _globals['_TYPEREFERENCELIST']._serialized_start = 1336 + _globals['_TYPEREFERENCELIST']._serialized_end = 1395 + _globals['_FILEORIGIN']._serialized_start = 1398 + _globals['_FILEORIGIN']._serialized_end = 1557 + _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 + _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 + _globals['_TEXTFILEORIGIN']._serialized_start = 1634 + _globals['_TEXTFILEORIGIN']._serialized_end = 1718 + _globals['_MAVENCOORDINATE']._serialized_start = 1720 + _globals['_MAVENCOORDINATE']._serialized_end = 1805 + _globals['_BUILDINFO']._serialized_start = 1808 + _globals['_BUILDINFO']._serialized_end = 1962 + _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 + _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 +# @@protoc_insertion_point(module_scope) +``` \ No newline at end of file diff --git a/performance/r8-analyzer/references/REPORT_FORMAT.md b/performance/r8-analyzer/references/REPORT_FORMAT.md new file mode 100644 index 0000000..c98e939 --- /dev/null +++ b/performance/r8-analyzer/references/REPORT_FORMAT.md @@ -0,0 +1,57 @@ +# R8 Analysis Report Template + +## 1. Configuration + +*(Optional section for the report, omit if no relevant findings are present.)* + +- **AGP Version**: [Current] -> Upgrade to 9.0. +- **Full Mode**: Not enabled. Remove `android.enableR8.fullMode=false` + from `gradle.properties`. + +## 2. Global disable rules + +*(Optional section for the report, omit if no relevant findings are present.)* + +- [Rule]: Disables R8 globally. **Action**: Remove. + +If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, +mention in this section + +## 3. Optimization summary + +- **Optimization Score**: [X]% code is available for R8 optimizations +(e.g., inlining, merging). [100-X]% of codebase can't be optimized by R8. +- **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. + +## 4. Keep rules evaluation + +### [Rule Text] + +- **Keeps**: [X] items or [X] % of the codebase from optimization. Classes: [X], Fields: [X], Methods: [X] are prevented from optimization due to this keep rule +- **Kept items**: [Class1], [Class2] +- **Action**: **Remove** (Library bundles rules) OR **Refine** (Too broad, use [Surgical Rule]). + +## 5. Subsumed keep rules + +*(Optional section for the report, omit if no relevant findings are present.)* + +### [Redundant rules] + +- **Subsumed By**: [Broader Rule] +- **Action**: **Remove**. + +## 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 From d9bcae655955434f24f7206a52828958731989c0 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 21 May 2026 20:14:43 +0000 Subject: [PATCH 23/61] Updates skills (2026-05-21 20:14) --- jetpack-compose/adaptive/SKILL.md | 2 +- .../layouts/adaptive/flexbox/get-started.md | 2 +- .../layouts/adaptive/grid/get-started.md | 2 +- .../SKILL.md | 2 +- ...setup-compose-dependencies-and-compiler.md | 6 +- jetpack-compose/theming/styles/SKILL.md | 2 +- .../develop/ui/compose/styles/fundamentals.md | 3 + .../develop/ui/compose/styles/theming.md | 2 +- performance/r8-analyzer/SKILL.md | 25 +- .../references/CONFIGURATION-ANALYZER.md | 458 ++++++------ .../r8-analyzer/references/REPORT_FORMAT.md | 48 +- .../enable-app-optimization.md | 17 + .../SKILL.md | 2 +- .../google/play/billing/release-notes.md | 34 + wear/jetpack-compose-m3/SKILL.md | 289 ++++++++ .../wearables/compose/migrate-to-material3.md | 677 ++++++++++++++++++ .../SKILL.md | 12 +- .../access-hardware-projected-context.md | 213 +++++- .../ai-glasses/check-capabilities.md | 47 -- .../ai-glasses/first-activity.md | 276 ------- .../ai-glasses/notifications/behavior.md | 130 ---- .../notifications/start-activity.md | 77 -- .../jetpack-compose-glimmer/buttons.md | 141 ++-- .../jetpack-compose-glimmer/cards.md | 124 ++-- .../jetpack-compose-glimmer/focus.md | 41 +- .../jetpack-compose-glimmer/icons.md | 107 ++- .../jetpack-compose-glimmer/text.md | 80 ++- .../jetpack-compose-glimmer/title-chips.md | 73 +- .../request-hardware-permissions.md | 31 +- 29 files changed, 1836 insertions(+), 1087 deletions(-) create mode 100644 wear/jetpack-compose-m3/SKILL.md create mode 100644 wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md index 93cf80f..64030c6 100644 --- a/jetpack-compose/adaptive/SKILL.md +++ b/jetpack-compose/adaptive/SKILL.md @@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-15' + last-updated: '2026-05-20' keywords: - android - ui 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 9164652..8317e17 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 @@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts. `lib.versions.toml`. [versions] - compose = "1.12.0-alpha02" + compose = "1.12.0-alpha03" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 1c51d22..28b8c3d 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 @@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co `lib.versions.toml`. [versions] - compose = "1.12.0-alpha02" + compose = "1.12.0-alpha03" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 6af64db..1597339 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-05-06' + last-updated: '2026-05-19' keywords: - Jetpack Compose - migration 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 0771751..5abc55e 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 @@ -75,7 +75,7 @@ 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.04.01`. +Always use the latest Compose BOM version: `2026.05.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. @@ -104,7 +104,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - def composeBom = platform('androidx.compose:compose-bom:2026.04.01') + def composeBom = platform('androidx.compose:compose-bom:2026.05.00') implementation composeBom androidTestImplementation composeBom @@ -143,7 +143,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - val composeBom = platform("androidx.compose:compose-bom:2026.04.01") + val composeBom = platform("androidx.compose:compose-bom:2026.05.00") implementation(composeBom) androidTestImplementation(composeBom) diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index 10d897b..70d3d44 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-05-14' + last-updated: '2026-05-19' keywords: - Jetpack Compose - Styles 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 e836c22..5361bf5 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 @@ -295,6 +295,9 @@ Styles](https://developer.android.com/static/develop/ui/compose/styles/images/bu ## Style inheritance +> [!NOTE] +> **Note:** Whilst 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. 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 8d62287..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 @@ -24,7 +24,7 @@ subsystems and components: **Styles**. | Layer | Responsibility | Example | |---|---|---| | **Subsystem values** | Named values | `val Primary = Color(0xFF34A85E)` | -| **Atomic Styles** | Style that does exactly one property change | `val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic` | +| **Atomic Styles** | Style that does exactly one property change | `val largeSizeAtomic = Style { size(100.dp, 40.dp) }` | | **Component Styles** | Component-specific configurations | A Button with Primary background and 16dp padding. `val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }` | | **Components** | The functional UI element that consumes a Style. | `Button(style = buttonStyle) { ... }` | diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 51dc623..3d7ce47 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-05-16' + last-updated: '2026-05-19' keywords: - R8 - proguard @@ -22,13 +22,12 @@ metadata: - Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. - **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) -- **Full mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. +- **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. ## Step 2. Analysis path selection -- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` - and `libs.versions.toml` to - get the R8 version +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and + `libs.versions.toml` to get the R8 version - **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. @@ -38,7 +37,7 @@ metadata: - **Check requirements** : Python and `protobuf` package are mandatory. - **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. -- **Report**: Rely entirely on the generated file analysis.txt for scores and rule impact metrics. Proceed to Step 3. +- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. ### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) @@ -52,20 +51,12 @@ metadata: ## Step 3. Report generation - **Format** : Follow [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) strictly. -- **Input**: Extract metrics (Scores, Impacts, Example Classes) - directly from generated file analysis.txt if using Path A, - or from manual findings if using Path B. -- **Output** : - Output ONLY the raw Markdown report in the chat. - Do NOT output conversational filler (for example, "Here is your report..."). - Do NOT provide recommendations, next steps, - or any other text outside of the sections defined in - [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) - Do NOT mention the path used for analysis of the configuration +- **Input**: Extract metrics (Scores, Impacts, Example Classes) directly from generated file analysis.txt if using Path A, or from manual findings if using Path B. +- **Output** : Output ONLY the raw Markdown report in the chat. Do NOT output conversational filler (for example, "Here is your report..."). Do NOT provide recommendations, next steps, or any other text outside of the sections defined in [references/REPORT_FORMAT.md](references/REPORT_FORMAT.md) Do NOT mention the path used for analysis of the configuration ## Constraints - **Strict output limit**: The final output MUST strictly be the Markdown report and nothing else. - **No code changes**: Research and suggest only; Do not modify files. - **No redundancy**: Do not explain R8 benefits or reference skill internal files in the report. -- **Focus**: Omit sections (for example, Subsumed Rules, Configuration) if no issues or items are found. \ No newline at end of file +- **Focus**: Omit sections (for example, Subsumed Rules, Configuration) if no issues or items are found. diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md index 6f1ca6f..4039dcf 100644 --- a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md +++ b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -1,5 +1,3 @@ -# Configuration Analyzer data generation - On each step, keep the user informed of the progress by displaying the output. ### 1. Requirements @@ -8,294 +6,282 @@ 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: -```bash -mkdir -p "$PWD/tmp/r8analysis" -``` + 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: -```bash -# Remove the intermediate JSON and the directory containing protobuf files -rm tmp/r8analysis/keepruleradius.json -rm tmp/r8analysis/*.pb + # Remove the intermediate JSON and the directory containing protobuf files + rm tmp/r8analysis/keepruleradius.json + rm tmp/r8analysis/*.pb -# Copy the previous result to history before deleting the analysis -if [ -f tmp/r8analysis/analysis_result.txt ]; -then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && - rm tmp/r8analysis/analysis_result.txt; fi + # Copy the previous result to history before deleting the analysis + if [ -f tmp/r8analysis/analysis_result.txt ]; + then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && + rm tmp/r8analysis/analysis_result.txt; fi - -``` -### 4. Generate configuration analyzer report +### 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 +"-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to +generate Configuration Analyzer report -```bash -./gradlew assembleRelease \ - -Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis -``` + ./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. -```python -import sys -import os -import glob -from google.protobuf import json_format -import keep_radius_pb2 + import sys + import os + import glob + from google.protobuf import json_format + import keep_radius_pb2 -def convert_pb_to_json(input_pb_path, output_json_path): - bundle = keep_radius_pb2.BlastRadiusContainer() - - try: - with open(input_pb_path, "rb") as pb_file: - binary_data = pb_file.read() - except Exception as e: - print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) - return False + def convert_pb_to_json(input_pb_path, output_json_path): + bundle = keep_radius_pb2.BlastRadiusContainer() - try: - bundle.ParseFromString(binary_data) - except Exception as e: - print(f"Error parsing protobuf: {e}", file=sys.stderr) - return False + try: + with open(input_pb_path, "rb") as pb_file: + binary_data = pb_file.read() + except Exception as e: + print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) + return False - try: - json_string = json_format.MessageToJson( - bundle, - always_print_fields_with_no_presence=True, - preserving_proto_field_name=True, - indent=4 - ) - with open(output_json_path, "w", encoding="utf-8") as json_file: - json_file.write(json_string) - return True - except Exception as e: - print(f"Error writing JSON: {e}", file=sys.stderr) - return False + try: + bundle.ParseFromString(binary_data) + except Exception as e: + print(f"Error parsing protobuf: {e}", file=sys.stderr) + return False -if __name__ == "__main__": - input_pb = sys.argv[1] if len(sys.argv) > 1 else None - if not input_pb: - pb_files = glob.glob("tmp/r8analysis/*.pb") - if not pb_files: - print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) + try: + json_string = json_format.MessageToJson( + bundle, + always_print_fields_with_no_presence=True, + preserving_proto_field_name=True, + indent=4 + ) + with open(output_json_path, "w", encoding="utf-8") as json_file: + json_file.write(json_string) + return True + except Exception as e: + print(f"Error writing JSON: {e}", file=sys.stderr) + return False + + if __name__ == "__main__": + input_pb = sys.argv[1] if len(sys.argv) > 1 else None + if not input_pb: + pb_files = glob.glob("tmp/r8analysis/*.pb") + if not pb_files: + print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) + sys.exit(1) + input_pb = sorted(pb_files)[-1] # Use the most recent one + output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" + if not convert_pb_to_json(input_pb, output_json): sys.exit(1) - input_pb = sorted(pb_files)[-1] # Use the most recent one - output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" - if not convert_pb_to_json(input_pb, output_json): - sys.exit(1) -``` ### 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. -```python -import json, sys + import json, sys -def analyze(path): - try: - with open(path, 'r') as f: - d = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return + def analyze(path): + try: + with open(path, 'r') as f: + d = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return - # Build reference map - c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} - r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} + # Build reference map + c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} + r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} - tot_opt = tot_obf = tot_shr = tot_items = 0 + tot_opt = tot_obf = tot_shr = tot_items = 0 - # Tally constraints across all kept items - for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): - for i in d.get(tbl, []): - tot_items += 1 - kb = i.get('kept_by', []) - if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 - if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 - if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 + # Tally constraints across all kept items + for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): + for i in d.get(tbl, []): + tot_items += 1 + kb = i.get('kept_by', []) + if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 + if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 + if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 - # Find denominator - bi = d.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else tot_items + # Find denominator + bi = d.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else tot_items - # Check for globals - globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] - def score(cnt, flag): - if any(flag in src for src in globals_src): return 0.0 - return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) + # Check for globals + globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] + def score(cnt, flag): + if any(flag in src for src in globals_src): return 0.0 + return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) - result = [ - f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", - f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", - f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" - ] - for line in result: - print(line) - with open("tmp/r8analysis/analysis_result.txt", "w") as f: - f.write("\n".join(result)) + result = [ + f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", + f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", + f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" + ] + for line in result: + print(line) + with open("tmp/r8analysis/analysis_result.txt", "w") as f: + f.write("\n".join(result)) + + if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" + analyze(path) -if __name__ == "__main__": - path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" - analyze(path) -``` 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. -```python -import json, sys + import json, sys -def report(path): - try: - with open(path, 'r') as f: - data = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return + def report(path): + try: + with open(path, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return - # Calculate denominator for percentage - bi = data.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) + # Calculate denominator for percentage + bi = data.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) - processed = [] - for r in data.get('keep_rule_blast_radius_table', []): - br = r.get('blast_radius', {}) - c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) - impact = c + f + m - if impact == 0: - continue - impact_pct = (impact / denom * 100) if denom > 0 else 0.0 - processed.append({ - 'id': r.get('id'), - 'source': r.get('source'), - 'impact': impact, - 'impact_pct': f"{impact_pct:.2f}%", - 'classes': c, - 'fields': f, - 'methods': m, - 'subsumed_by': br.get('subsumed_by', []) - }) + processed = [] + for r in data.get('keep_rule_blast_radius_table', []): + br = r.get('blast_radius', {}) + c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) + impact = c + f + m + if impact == 0: + continue + impact_pct = (impact / denom * 100) if denom > 0 else 0.0 + processed.append({ + 'id': r.get('id'), + 'source': r.get('source'), + 'impact': impact, + 'impact_pct': f"{impact_pct:.2f}%", + 'classes': c, + 'fields': f, + 'methods': m, + 'subsumed_by': br.get('subsumed_by', []) + }) - processed.sort(key=lambda x: x['impact'], reverse=True) + processed.sort(key=lambda x: x['impact'], reverse=True) - # Output JSON for the agent to fetch and process - print(json.dumps({ - "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], - "subsumed": [r for r in processed if r['subsumed_by']] - }, indent=2)) + # Output JSON for the agent to fetch and process + print(json.dumps({ + "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], + "subsumed": [r for r in processed if r['subsumed_by']] + }, indent=2)) + + if __name__ == "__main__": + report("tmp/r8analysis/keepruleradius.json") -if __name__ == "__main__": - report("tmp/r8analysis/keepruleradius.json") -``` Add this data to the `analysis_result.txt` with the top impactful rules and subsumed rules. ### 8. Compare with previous report -If `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 "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 -```bash -rm tmp/r8analysis/keepruleradius.json -rm tmp/r8analysis/*.pb -``` + 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. -```python -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 4, - '', - 'keep_radius.proto' -) -# @@protoc_insertion_point(imports) + from google.protobuf import descriptor as _descriptor + from google.protobuf import descriptor_pool as _descriptor_pool + from google.protobuf import runtime_version as _runtime_version + from google.protobuf import symbol_database as _symbol_database + from google.protobuf.internal import builder as _builder + _runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 4, + '', + 'keep_radius.proto' + ) + # @@protoc_insertion_point(imports) -_sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') + _sym_db = _symbol_database.Default() + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' - _globals['_KEEPRULETAG']._serialized_start = 3332 - _globals['_KEEPRULETAG']._serialized_end = 3363 - _globals['_KEEPCONSTRAINT']._serialized_start = 3365 - _globals['_KEEPCONSTRAINT']._serialized_end = 3437 - _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 - _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 - _globals['_BLASTRADIUS']._serialized_start = 351 - _globals['_BLASTRADIUS']._serialized_end = 470 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 - _globals['_KEEPCONSTRAINTS']._serialized_start = 601 - _globals['_KEEPCONSTRAINTS']._serialized_end = 707 - _globals['_KEPTCLASSINFO']._serialized_start = 709 - _globals['_KEPTCLASSINFO']._serialized_end = 805 - _globals['_KEPTFIELDINFO']._serialized_start = 807 - _globals['_KEPTFIELDINFO']._serialized_end = 903 - _globals['_KEPTMETHODINFO']._serialized_start = 905 - _globals['_KEPTMETHODINFO']._serialized_end = 1003 - _globals['_FIELDREFERENCE']._serialized_start = 1005 - _globals['_FIELDREFERENCE']._serialized_end = 1102 - _globals['_METHODREFERENCE']._serialized_start = 1104 - _globals['_METHODREFERENCE']._serialized_end = 1203 - _globals['_PROTOREFERENCE']._serialized_start = 1205 - _globals['_PROTOREFERENCE']._serialized_end = 1280 - _globals['_TYPEREFERENCE']._serialized_start = 1282 - _globals['_TYPEREFERENCE']._serialized_end = 1334 - _globals['_TYPEREFERENCELIST']._serialized_start = 1336 - _globals['_TYPEREFERENCELIST']._serialized_end = 1395 - _globals['_FILEORIGIN']._serialized_start = 1398 - _globals['_FILEORIGIN']._serialized_end = 1557 - _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 - _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 - _globals['_TEXTFILEORIGIN']._serialized_start = 1634 - _globals['_TEXTFILEORIGIN']._serialized_end = 1718 - _globals['_MAVENCOORDINATE']._serialized_start = 1720 - _globals['_MAVENCOORDINATE']._serialized_end = 1805 - _globals['_BUILDINFO']._serialized_start = 1808 - _globals['_BUILDINFO']._serialized_end = 1962 - _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 - _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 -# @@protoc_insertion_point(module_scope) -``` \ No newline at end of file + _globals = globals() + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) + _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) + if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' + _globals['_KEEPRULETAG']._serialized_start = 3332 + _globals['_KEEPRULETAG']._serialized_end = 3363 + _globals['_KEEPCONSTRAINT']._serialized_start = 3365 + _globals['_KEEPCONSTRAINT']._serialized_end = 3437 + _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 + _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 + _globals['_BLASTRADIUS']._serialized_start = 351 + _globals['_BLASTRADIUS']._serialized_end = 470 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 + _globals['_KEEPCONSTRAINTS']._serialized_start = 601 + _globals['_KEEPCONSTRAINTS']._serialized_end = 707 + _globals['_KEPTCLASSINFO']._serialized_start = 709 + _globals['_KEPTCLASSINFO']._serialized_end = 805 + _globals['_KEPTFIELDINFO']._serialized_start = 807 + _globals['_KEPTFIELDINFO']._serialized_end = 903 + _globals['_KEPTMETHODINFO']._serialized_start = 905 + _globals['_KEPTMETHODINFO']._serialized_end = 1003 + _globals['_FIELDREFERENCE']._serialized_start = 1005 + _globals['_FIELDREFERENCE']._serialized_end = 1102 + _globals['_METHODREFERENCE']._serialized_start = 1104 + _globals['_METHODREFERENCE']._serialized_end = 1203 + _globals['_PROTOREFERENCE']._serialized_start = 1205 + _globals['_PROTOREFERENCE']._serialized_end = 1280 + _globals['_TYPEREFERENCE']._serialized_start = 1282 + _globals['_TYPEREFERENCE']._serialized_end = 1334 + _globals['_TYPEREFERENCELIST']._serialized_start = 1336 + _globals['_TYPEREFERENCELIST']._serialized_end = 1395 + _globals['_FILEORIGIN']._serialized_start = 1398 + _globals['_FILEORIGIN']._serialized_end = 1557 + _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 + _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 + _globals['_TEXTFILEORIGIN']._serialized_start = 1634 + _globals['_TEXTFILEORIGIN']._serialized_end = 1718 + _globals['_MAVENCOORDINATE']._serialized_start = 1720 + _globals['_MAVENCOORDINATE']._serialized_end = 1805 + _globals['_BUILDINFO']._serialized_start = 1808 + _globals['_BUILDINFO']._serialized_end = 1962 + _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 + _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 + # @@protoc_insertion_point(module_scope) \ 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 c98e939..629c777 100644 --- a/performance/r8-analyzer/references/REPORT_FORMAT.md +++ b/performance/r8-analyzer/references/REPORT_FORMAT.md @@ -1,57 +1,51 @@ -# R8 Analysis Report Template - ## 1. Configuration *(Optional section for the report, omit if no relevant findings are present.)* -- **AGP Version**: [Current] -> Upgrade to 9.0. -- **Full Mode**: Not enabled. Remove `android.enableR8.fullMode=false` - from `gradle.properties`. +- **AGP Version**: \[Current\] -\> Upgrade to 9.0. +- **Full Mode** : Not enabled. Remove `android.enableR8.fullMode=false` from `gradle.properties`. ## 2. Global disable rules *(Optional section for the report, omit if no relevant findings are present.)* -- [Rule]: Disables R8 globally. **Action**: Remove. +- \[Rule\]: Disables R8 globally. **Action**: Remove. If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, mention in this section ## 3. Optimization summary -- **Optimization Score**: [X]% code is available for R8 optimizations -(e.g., inlining, merging). [100-X]% of codebase can't be optimized by R8. -- **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. +- **Optimization score**: \[X\]% code is available for R8 optimizations (e.g., inlining, merging). \[100-X\]% of codebase can't be optimized by R8. +- **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 -### [Rule Text] +### \[Rule text\] -- **Keeps**: [X] items or [X] % of the codebase from optimization. Classes: [X], Fields: [X], Methods: [X] are prevented from optimization due to this keep rule -- **Kept items**: [Class1], [Class2] -- **Action**: **Remove** (Library bundles rules) OR **Refine** (Too broad, use [Surgical Rule]). +- **Keeps**: \[X\] items or \[X\] % of the codebase from optimization. Classes: \[X\], Fields: \[X\], Methods: \[X\] are prevented from optimization due to this keep rule +- **Kept items**: \[Class1\], \[Class2\] +- **Action** : **Remove** (Library bundles rules) OR **Refine** (Too broad, use \[Surgical Rule\]). ## 5. Subsumed keep rules *(Optional section for the report, omit if no relevant findings are present.)* -### [Redundant rules] +### \[Redundant rules\] -- **Subsumed By**: [Broader Rule] -- **Action**: **Remove**. +- **Subsumed By**: \[Broader Rule\] +- **Action** : **Remove**. ## 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 +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 d78158e..9728527 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 @@ -161,6 +161,23 @@ fix them: 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. +## 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, enable +R8 in full mode and refine the keep rules to allow maximum optimization of +classes, fields and methods.To help you with this, use the +[R8 Configuration Analyzer](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). + +The R8 Configuration Analyzer lets you do the following: + +- Track and improve the overall R8 configuration quality by monitoring the metrics provided by the R8 Configuration Analyzer report. +- 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). + ## AGP and R8 version behavior changes The following table outlines the key features introduced in various versions of diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md index 2feb044..0a63a95 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-04-13' + last-updated: '2026-05-20' keywords: - android - play billing 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 4a1dd9a..c1106b1 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,5 +1,39 @@ This document contains release notes for the Google Play Billing Library. +## 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. + +### 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. + + > [!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. + + 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. + ## Google Play Billing Library 8.3.0 Release (2025-12-23) Version 8.3.0 of the Google Play Billing Library and Kotlin extensions are now available. diff --git a/wear/jetpack-compose-m3/SKILL.md b/wear/jetpack-compose-m3/SKILL.md new file mode 100644 index 0000000..6dd5ade --- /dev/null +++ b/wear/jetpack-compose-m3/SKILL.md @@ -0,0 +1,289 @@ +--- +name: jetpack-compose-m3 +description: Expert guidance for working with Wear OS Compose Material3. Use this + skill when creating, updating or migrating Wear OS projects. This includes the androidx.wear.compose.material3, + androidx.wear.compose.foundation and androidx.wear.compose.navigation3 libraries. + Also working with core components such as AppScaffold, ScreenScaffold and TransformingLazyColumn. + Migration from earlier versions such as Material 2.5 and Horologist. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-05-20' + keywords: + - Wear OS + - Compose + - Material3 + - Horologist + - TransformingLazyColumn + - AppScaffold + - ScreenScaffold +--- + +## Prerequisites \& Compatibility + +1. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. + - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`) + 1. **Strict Compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. +2. **Kotlin Version:** For Wear Compose Material3, use Kotlin **2.0.0 or + higher**. +3. **Compose Compiler:** + - If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin. + - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility + Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). + 1. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). +4. **Sample Extraction Mandate**: Wear Compose libraries ship with an + additional JAR file which contains individual samples for each and every + component. You MUST NOT propose code changes until the samples in Capability + + files are incomplete and NOT a substitute for these samples; bypassing + extraction is an environment setup failure. + +## Gotchas + +1. **Mandatory Sync \& Validation:** After updating versions in `libs.versions.toml` or `build.gradle.kts`, you **must** perform a Gradle sync before refactoring any code. This ensures the environment has resolved the libraries correctly. +2. **Prohibition of Guessing (Error Protocol):** If you encounter an 'Unresolved Reference' or API mismatch after a successful sync, do not attempt to 'fix' it by downgrading the library version. + +## Capabilities and Tools + +### Capability 1: Migration + +Use this guidance when migrating from an older version of Wear OS Compose or +Horologist. + +1. Unless otherwise indicated by the developer, use the latest stable version of Wear Compose Material3 from `{VERSION}`. +2. Read the [migration guide](references/android/training/wearables/compose/migrate-to-material3.md) +3. Use the official component mappings from the migration guide. +4. Before refactoring any component (e.g., `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. +5. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. +6. **Always** check against the component guidance in Capability #3. +7. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. + +### Capability 2: Component samples + +Wear Compose includes individual component samples for each and every component, +within the `--samples-sources.jar` file. Gradle automatically +downloads these JAR files along with the main library JAR when using any of +`compose-material3`, `compose-foundation` or `compose-navigation3`. + +Use the canonical component samples whenever adding or adjusting a Wear Compose +Material3 component. + +STRICT COMPLIANCE: Extraction is NOT optional. You are FORBIDDEN from +implementing any code until samples are extracted and read. Bypassing this step +with alternative search tools or by assuming library documentation is sufficient +is a protocol breach. You MUST verify the local cache by reading a sample file +before proceeding. + +#### Step 1: Prepare + +1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. +2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. + +#### Step 2: Check the Local Cache + +1. Define the cache directory path: `/samples/{VERSION}/`. Do NOT choose your own different location. +2. Check if this directory exists and contains subdirectories with `.kt` files. + - **IF YES (Cache Hit):** Proceed to **Step 4**. + - **IF NO (Cache Miss):** Proceed to **Step 3**. + +#### Step 3: Check the Gradle Cache + +1. Sample sources are stored in the Gradle cache. To avoid slow, brute-force searches: + - Determine the Gradle User Home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). + - The cache root is `/caches`. Call this ``. +2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include "navigation3" in the list if the `androidx.wear.compose.navigation3` library is being used. +3. For each `{ARTIFACT}` in the list: + + - Construct the expected relative path segment for the library: `androidx.wear.compose/compose-{ARTIFACT}/{VERSION}`. + - Run a targeted `find` command. Here is an example which is constructed + for efficiency: + + find /modules-2/files-2.1/androidx.wear.compose/compose-{ARTIFACT}/{VERSION}/ \ + -name "*samples-sources.jar" + +4. Use this JAR as the official sample sources. + +5. Extract the contents of each JAR to + `/samples/{VERSION}/{ARTIFACT}/` using `unzip -j` to flatten the + structure. + +6. Proceed **directly to step 4**. + +#### Step 4: Read Samples and Implement + +1. Read the relevant `.kt` sample files. +2. Use these official, version-matched samples as the for: + - Required parameters and slot names. + - Default styling and typography tokens. + - Interactive behaviors (e.g., `onClick`, `onLongClick`). + - Component nesting (e.g., `AppScaffold` -\> `ScreenScaffold`). + +### Capability 3: Component guidance + +**Mandatory**: Use this capability as a checklist against any component use. It +provides more holistic guidance on how to use each component in practice, beyond +the component syntax. + +1. `AppScaffold` and `ScreenScaffold` + - \[ \] Use `AppScaffold` as the outer container, with `ScreenScaffold` children. + - \[ \] Use only **ONE** `AppScaffold` and any number of `ScreenScaffold`. +2. `ScalingLazyColumn` - Use `TransformingLazyColumn` instead. +3. `TransformingLazyColumn` + + You will need the following imports: + + + ```kotlin + import androidx.wear.compose.foundation.lazy.TransformingLazyColumn + import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults + import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState + // ... + import androidx.wear.compose.material3.lazy.rememberTransformationSpec + import androidx.wear.compose.material3.lazy.transformedHeight + ``` + +
+ + **Canonical example**: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + ScreenScaffold( + scrollState = columnState + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + ``` + +
+ + - \[ \] Use `TransformingLazyColumn` instead of `ScalingLazyColumn` + - \[ \] You must pass the `contentPadding` parameter from `ScreenScaffold` to the `TransformingLazyColumn`. + - \[ \] Use the `minimumVerticalContentPadding` modifier to achieve required padding top and bottom. + - This expects a value from defaults, such as `ButtonDefaults`, `CardDefaults`, \`ListHeaderDefaults. + - Note: This is a scoped modifier available within `TransformingLazyColumnItemScope`. + - \[ \] Ensure the list morphs and scales. + - \[ \] Use `transformedHeight` modifier. + - \[ \] Use `transform = SurfaceTransform(...)`. + - \[ \] If configuring a list for snapping, use `flingBehavior` and `rotaryScrollableBehavior` **together**: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + ScreenScaffold(scrollState = columnState) { contentPadding -> + TransformingLazyColumn( + state = columnState, + flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), + rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) + ) { + // ... + // ... + } + } + ``` + +
+ +4. `ScreenScaffold` + + - \[ \] Guard the `scrollIndicator` with `!LocalScrollCaptureInProgress.current` +5. `EdgeButton` + + - \[ \] Do **NOT** use as the final item within a `TransformingLazyColumn`. Instead, use the slot in `ScreenScaffold`. + - \[ \] When used in a `TransformingLazyColumn`, add the required overscroll behavior: + + + ```kotlin + val columnState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollState = columnState, + edgeButton = { + EdgeButton( + onClick = { /* TODO */ }, + modifier = Modifier.scrollable( + columnState, + orientation = Orientation.Vertical, + reverseDirection = true, + // Apply overscroll to the EdgeButton for proper scrolling behavior. + overscrollEffect = rememberOverscrollEffect(), + ) + ) { + Text("More") + } + } + ) { contentPadding -> + TransformingLazyColumn( + contentPadding = contentPadding, + state = columnState, + ) { + // ... + // ... + } + } + ``` + +
+ +6. `Column` + + - \[ \] USE as a direct child of `ScreenScaffold` *if* the screen is will **never** scroll, even with the largest system font. + - \[ \] Use `TransformingLazyColumn` instead for all other cases. +7. Styles + + - \[ \] Do **NOT** hard-code text sizes, use `typography` from `MaterialTheme`. + - \[ \] Do **NOT** hard-code colors, use `colorScheme` from `MaterialTheme`. +8. Use component defaults + + - \[ \] Components such as `Button` have a corresponding `ButtonDefaults` object. + - Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. +9. Use Wear specific previews + + - \[ \] `WearPreviewDevices` + - \[ \] `WearPreviewFontScales` +10. Ambient mode + + - \[ \] Use `LocalAmbientModeManager` instead of `AmbientLifecycleObserver`. +11. Navigation + + - \[ \] When adding navigation fresh, use Navigation3. + - \[ \] For Navigation3 in Wear OS, use `SwipeDismissableSceneStrategy()` from the Wear Compose `compose-navigation3` library. diff --git a/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md new file mode 100644 index 0000000..3123a04 --- /dev/null +++ b/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -0,0 +1,677 @@ +[Material 3 Expressive](https://developer.android.com/design/ui/wear/guides/get-started) is the next evolution of Material Design. It includes +updated theming, components, and personalization features like dynamic color. + +This guide focuses on migrating from the [Wear Compose Material 2.5 +(androidx.wear.compose)](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2) Jetpack library to the [Wear Compose Material 3 +(androidx.wear.compose.material3)](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3) Jetpack library for apps. + +> [!NOTE] +> **Note:** This guide uses abbreviation "M3" to refer to the interchangeable terms of "Material 3 Expressive" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material3). The abbreviation "M2.5" is used to refer to the interchangeable terms of "Material 2.5" and the equivalent Jetpack library for Compose on Wear OS (androidx.wear.compose.material). + +## Approaches + +For migrating your app code from M2.5 to M3, follow the same approach described +in the [Compose Material migration phone guidance](https://developer.android.com/develop/ui/compose/designsystems/material2-material3), in particular: + +- You shouldn't use both [M2.5 and M3 in a single app long-term](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#approaches). +- You should no longer use the Horologist Composables, Compose Layout, or Compose Material libraries. Instead, use the components in M3. +- Adopt a [phased approach](https://developer.android.com/develop/ui/compose/designsystems/material2-material3#phased-approach). + +## Dependencies + +M3 has a separate package and version to M2.5: + +### M2.5 + + implementation("androidx.wear.compose:compose-material:1.4.0") + +### M3 + + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha03") + +See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). + +Wear Compose Foundation library version 1.7.0-alpha03 introduced +some new components that are designed to work with Material 3 components. +Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an +updated animation when running on Wear OS 6 (API level 36) or higher. When +updating to Wear Compose Material 3 version, we suggest to also update the Wear +Compose Foundation and Navigation libraries: + + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha03") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha03") + +## Theme + +In both M2.5 and M3, the theme composable is named [`MaterialTheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)), but the +import packages and parameters differ. In M3, the `Colors` parameter has been +renamed to `ColorScheme` and `MotionScheme` has been introduced for implementing +transitions. + +### M2.5 + + import androidx.wear.compose.material.MaterialTheme + + MaterialTheme( + colors = AppColors, + typography = AppTypography, + shapes = AppShapes, + content = content + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.MaterialTheme +// ... + MaterialTheme( + colorScheme = ColorScheme(), + typography = Typography(), + shapes = Shapes(), + motionScheme = MotionScheme.standard(), + content = { /*content here*/ } + ) +``` + +
+ +### Color + +The color system in M3 is significantly different from M2.5. The number of color +parameters has increased, they have different names, and they map differently to +M3 components. In Compose, this applies to the M2.5 [`Colors`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Colors) class, the M3 +[`ColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/ColorScheme) class, and related functions: + +### M2.5 + + import androidx.wear.compose.material.Colors + + val appColorScheme: Colors = Colors( + // M2.5 Color parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.ColorScheme +// ... + val appColorScheme: ColorScheme = ColorScheme( + // M3 ColorScheme parameters + ) +``` + +
+ +The following table describes the key differences between M2.5 and M3: + +| M2.5 | M3 | +|---|---| +| `Color` | Has been renamed to `ColorScheme` | +| 13 colors | 28 colors | +| N/A | New dynamic color theming | +| N/A | New tertiary colors for more expression | + +#### Dynamic color theming + +A new feature in M3 is [dynamic color theming](https://m3.material.io/styles/color/dynamic-color/overview). If users change +the watch face colors, the colors in the UI change to match. + +Use the [`dynamicColorScheme`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#dynamicColorScheme(android.content.Context)) function to implement dynamic color scheme +and provide a `defaultColorScheme` as a fallback in case dynamic color scheme is +not available. + + +```kotlin +@Composable +fun myApp() { + val dynamicColorScheme = dynamicColorScheme(LocalContext.current) + MaterialTheme(colorScheme = dynamicColorScheme ?: myBrandColors) {} +} + +internal val myBrandColors: ColorScheme = ColorScheme( /* Specify colors here */) +``` + +
+ +### Typography + +The [typography system](https://m3.material.io/styles/typography/overview) in M3 is different from M2.5 and it includes +the following features: + +- Nine new [text styles](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/Typography#public-properties_1) +- Flex fonts, which allow for customization of the type scales for different weights, widths, and roundness +- `AnimatedText`, which uses flex fonts + +### M2.5 + + import androidx.wear.compose.material.Typography + + val Typography = Typography( + // M2.5 TextStyle parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.Typography + +val Typography = Typography( + // M3 TextStyle parameters +) +``` + +
+ +#### Flex fonts + +Flex Fonts allow designers to specify the type width and weight for specific +sizes. + +#### Text styles + +The following [TextStyles](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt;l=115?q=displayLarge&ss=androidx/platform/frameworks/support) are available in M3. These are +employed by default by various M3 components. + +| Typography | TextStyle | +|---|---| +| Display | displayLarge, displayMedium, displaySmall | +| Title | titleLarge, titleMedium, titleSmall | +| Label | labelLarge, labelMedium, labelSmall | +| Body | bodyLarge, bodyMedium, bodySmall, bodyExtraSmall | +| Numeral | numeralExtraLarge, numeralLarge, numeralMedium, numeralSmall, numeralExtraSmall | +| Arc | arcLarge, arcMedium, arcSmall | + +### Shape + +The [shape system](https://m3.material.io/styles/shape/overview) in M3 is different from M2.5. The number of shape +parameters has increased, they're named differently, and they map differently to +M3 components. The following shape sizes are available: + +- Extra-small +- Small +- Medium +- Large +- Extra-large + +In Compose, this applies to the M2 [`Shapes`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/Shapes) class and the M3 +[`Shapes`](https://developer.android.com/reference/kotlin/androidx/compose/material3/Shapes) class: + +### M2.5 + + import androidx.wear.compose.material.Shapes + + val Shapes = Shapes( + // M2.5 Shapes parameters + ) + +### M3 + + +```kotlin +import androidx.wear.compose.material3.Shapes + +val Shapes = Shapes( + // M3 Shapes parameters +) +``` + +
+ +> [!NOTE] +> **Note:** For shapes, we generally recommend using the default Material 3 Wear shapes which are optimized for round devices. + +Use the Shapes parameter mapping from [Migrate from Material 2 to Material 3 in +Compose](https://developer.android.com/training/wearables/compose/migrate-to-material3#shape) as a starting point. + +### Shape morphing + +M3 introduces Shape Morphing: shapes now morph in response to interactions. + +Shape Morphing behavior is available as a variation on a number of round +buttons, see the following list of buttons that support Shape Morphing: + +| Buttons | Shape morphing function | +|---|---| +| `IconButton` | [IconButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon button on press | +| `IconToggleButton` | [IconToggleButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the icon toggle button on press and [IconToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/IconToggleButtonDefaults#variantAnimatedShapes()) animates the icon toggle button on press and check/uncheck | +| `TextButton` | [TextButtonDefaults.animatedShape](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text button on press | +| `TextToggleButton` | [TextToggleButtonDefaults.animatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#animatedShapes(androidx.compose.foundation.shape.CornerBasedShape,androidx.compose.foundation.shape.CornerBasedShape)) animates the text toggle on press and [TextToggleButtonDefaults.variantAnimatedShapes](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/TextToggleButtonDefaults#variantAnimatedShapes()) animates the text toggle on press and check/uncheck | + +## Components and Layout + +Most components and layouts from M2.5 are available in M3. However, some M3 +components and layouts didn't exist in M2.5. Furthermore, some M3 components +have more variations than their equivalents in M2.5. + +While some components require special considerations, the following function +mappings are recommended as a starting point: + +| Material 2.5 | Material 3 | +|---|---| +| [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | +| [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.TitleCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TitleCard(kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function0,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0)) | +| [androidx.wear.compose.material.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | [androidx.wear.compose.material3.AppCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppCard(kotlin.Function0,kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Checkbox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Checkbox(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.CheckboxColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.OutlinedButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OutlinedButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) or [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) or [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) | +| [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | +| [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | +| [androidx.wear.compose.material.LocalContentAlpha()](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#LocalContentAlpha()) | Has been removed as not used by `Text` or `Icon` in Material 3 | +| [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | +| [androidx.wear.compose.material.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#RadioButton(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.RadioButtonColors,kotlin.Boolean,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | +| [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | +| [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) | [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) and [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)), or [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Switch](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Switch(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.wear.compose.material.SwitchColors,kotlin.Boolean,kotlin.Function1,androidx.compose.foundation.interaction.MutableInteractionSource)) | No M3 equivalent, migrate to [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) or [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | +| [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) or [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | +| [androidx.wear.compose.material.Vignette](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Vignette(androidx.wear.compose.material.VignettePosition,androidx.compose.ui.Modifier)) | Removed as not included in Material 3 Expressive design for Wear OS | + +Here is a full list of all the Material 3 components: + +| Material 3 | Material 2.5 equivalent component (if not new in M3) | +|---|---| +| [androidx.wear.compose.material3.AlertDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AlertDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.window.DialogProperties,kotlin.Function1)) | [androidx.wear.compose.material.dialog.Alert](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Alert(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | +| [androidx.wear.compose.material3.AnimatedPage](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedPage(kotlin.Int,androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.graphics.Color,kotlin.Function0)) | New | +| [androidx.wear.compose.material3.AnimatedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AnimatedText(kotlin.String,androidx.wear.compose.material3.AnimatedTextFontRegistry,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment)) | New | +| [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) ) | +| [androidx.wear.compose.material3.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.ButtonGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ButtonGroup(androidx.compose.ui.Modifier,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.Alignment.Vertical,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CardColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Card](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Card(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.painter.Painter,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.CheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CheckboxButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.CheckboxButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a checkbox toggle control | +| [androidx.wear.compose.material3.ChildButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ChildButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) (only when no background is required) | +| [androidx.wear.compose.material3.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp)) | [androidx.wear.compose.material.CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CircularProgressIndicator(androidx.compose.ui.Modifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp)) | +| [androidx.wear.compose.material3.CompactButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#CompactButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Function1,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.CompactChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CompactChip(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ChipBorder)) | +| [androidx.wear.compose.material3.ConfirmationDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ConfirmationDialog(kotlin.Boolean,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ConfirmationDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Confirmation](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Confirmation(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.foundation.lazy.ScalingLazyListState,kotlin.Long,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | +| [androidx.wear.compose.material3.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | [androidx.wear.compose.material.curvedText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.wear.compose.foundation.CurvedScope).curvedText(kotlin.String,androidx.wear.compose.foundation.CurvedModifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.wear.compose.foundation.CurvedTextStyle,androidx.wear.compose.foundation.CurvedDirection.Angular,androidx.compose.ui.text.style.TextOverflow)) | +| [androidx.wear.compose.material3.DatePicker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#DatePicker(java.time.LocalDate,kotlin.Function1,androidx.compose.ui.Modifier,java.time.LocalDate,java.time.LocalDate,androidx.wear.compose.material3.DatePickerType,androidx.wear.compose.material3.DatePickerColors)) | New | +| [androidx.wear.compose.material3.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | [androidx.wear.compose.material.dialog.Dialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/dialog/package-summary#Dialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.ui.window.DialogProperties,kotlin.Function0)) | +| [androidx.wear.compose.material3.EdgeButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.FadingExpandingLabel](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FadingExpandingLabel(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,kotlin.Boolean,kotlin.Int,kotlin.Int,androidx.compose.ui.text.TextStyle,androidx.compose.animation.core.FiniteAnimationSpec)) | New | +| [androidx.wear.compose.material3.FilledTonalButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#FilledTonalButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Chip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Chip(kotlin.Function0,androidx.wear.compose.material.ChipColors,androidx.wear.compose.material.ChipBorder,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.semantics.Role,kotlin.Function1)) when a tonal button background is required | +| [androidx.wear.compose.material3.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPageIndicator(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.HorizontalPageIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.HorizontalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | +| [androidx.wear.compose.material3.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | [androidx.wear.compose.material.Icon](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color)) | +| [androidx.wear.compose.material3.IconButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.IconButtonShapes,androidx.wear.compose.material3.IconButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | +| [androidx.wear.compose.material3.IconToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.IconToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.IconToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.LevelIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LevelIndicator(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.ranges.ClosedFloatingPointRange,kotlin.Boolean,androidx.wear.compose.material3.LevelIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Float,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.LinearProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#LinearProgressIndicator(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.ListHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ListHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)) | +| [androidx.wear.compose.material3.ListSubHeader](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ListSubHeader(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#MaterialTheme(androidx.wear.compose.material3.ColorScheme,androidx.wear.compose.material3.Typography,androidx.wear.compose.material3.Shapes,androidx.wear.compose.material3.MotionScheme,kotlin.Function0)) | [androidx.wear.compose.material.MaterialTheme](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#MaterialTheme(androidx.wear.compose.material.Colors,androidx.wear.compose.material.Typography,androidx.wear.compose.material.Shapes,kotlin.Function0)) | +| [androidx.wear.compose.material3.OpenOnPhoneDialog](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#OpenOnPhoneDialog(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material3.OpenOnPhoneDialogColors,androidx.compose.ui.window.DialogProperties,kotlin.Long,kotlin.Function1)) | New | +| [androidx.wear.compose.material3.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Picker(androidx.wear.compose.material3.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | [androidx.wear.compose.material.Picker](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Picker(androidx.wear.compose.material.PickerState,kotlin.String,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Function0,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.compose.ui.unit.Dp,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | +| [androidx.wear.compose.material3.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#PickerGroup(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function1,kotlin.Boolean,kotlin.Function1)) | [androidx.wear.compose.material.PickerGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PickerGroup(kotlin.Array,androidx.compose.ui.Modifier,androidx.wear.compose.material.PickerGroupState,kotlin.Function1,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.TouchExplorationStateProvider,kotlin.Function1)) | +| [androidx.wear.compose.material3.RadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.RadioButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a radio button toggle control | +| [androidx.wear.compose.material3.ScreenScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScreenScaffold(androidx.compose.ui.Modifier,kotlin.Function0,androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function1,kotlin.Function1)) | [android.wear.compose.material.Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0)) (with [androidx.wear.compose.material3.AppScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#AppScaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function1))) | +| [androidx.wear.compose.material3.ScrollIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#ScrollIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec)) | [androidx.wear.compose.material.PositionIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#PositionIndicator(androidx.compose.foundation.lazy.LazyListState,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec,androidx.compose.animation.core.AnimationSpec)) | +| [androidx.wear.compose.material3.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.wear.compose.foundation.ScrollInfoProvider,kotlin.Function0)) | [androidx.wear.compose.material.scrollAway](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.compose.ui.Modifier).scrollAway(androidx.compose.foundation.lazy.LazyListState,kotlin.Int,androidx.compose.ui.unit.Dp)) | +| [androidx.wear.compose.material3.SegmentedCircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SegmentedCircularProgressIndicator(kotlin.Int,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Float,kotlin.Float,androidx.wear.compose.material3.ProgressIndicatorColors,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Boolean)) | New | +| [androidx.wear.compose.material3.Slider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Slider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SliderColors)) | [androidx.wear.compose.material.InlineSlider](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#InlineSlider(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Boolean,androidx.wear.compose.material.InlineSliderColors)) | +| [androidx.wear.compose.material3.SplitRadioButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitRadioButton(kotlin.Boolean,kotlin.Function0,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitRadioButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.SplitCheckboxButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitCheckboxButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitCheckboxButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.SplitSwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SplitSwitchButton(kotlin.Boolean,kotlin.Function1,kotlin.String,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SplitSwitchButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.String,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.SplitToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SplitToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,androidx.wear.compose.material.SplitToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) | +| [androidx.wear.compose.material3.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) | [androidx.wear.compose.material.Stepper](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Boolean,kotlin.Function1)) | +| [androidx.wear.compose.material3.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | [androidx.wear.compose.material.SwipeToDismissBox](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Any,kotlin.Any,kotlin.Boolean,kotlin.Function2)) | +| [androidx.wear.compose.material3.SwipeToReveal](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwipeToReveal(kotlin.Function1,androidx.compose.ui.Modifier,androidx.wear.compose.foundation.RevealState,androidx.compose.ui.unit.Dp,kotlin.Function0)) | [androidx.wear.compose.material.SwipeToRevealCard](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Stepper(kotlin.Int,kotlin.Function1,kotlin.ranges.IntProgression,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.StepperColors,kotlin.Function1)) and [androidx.wear.compose.material.SwipeToRevealChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#SwipeToRevealChip(kotlin.Function1,androidx.wear.compose.foundation.RevealState,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.SwipeToRevealActionColors,androidx.compose.ui.graphics.Shape,kotlin.Function0)) | +| [androidx.wear.compose.material3.SwitchButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#SwitchButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.SwitchButtonColors,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1,kotlin.Function1,kotlin.Function1)) | [androidx.wear.compose.material.ToggleChip](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleChip(kotlin.Boolean,kotlin.Function1,kotlin.Function1,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1,androidx.wear.compose.material.ToggleChipColors,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.foundation.layout.PaddingValues,androidx.compose.ui.graphics.Shape)) with a switch toggle control | +| [androidx.wear.compose.material3.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | [androidx.wear.compose.material.Text](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)) | +| [androidx.wear.compose.material3.TextButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.wear.compose.material3.TextButtonShapes,androidx.wear.compose.material3.TextButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) | [androidx.wear.compose.material.Button](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material.ButtonBorder,kotlin.Function1)) | +| [androidx.wear.compose.material3.TextToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TextToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material3.TextToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.TextToggleButtonShapes,androidx.compose.foundation.BorderStroke,kotlin.Function1)) | [androidx.wear.compose.material.ToggleButton](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#ToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.wear.compose.material.ToggleButtonColors,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.semantics.Role,kotlin.Function1)) | +| [androidx.wear.compose.material3.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.CurvedModifier,kotlin.Float,androidx.wear.compose.material3.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) | [androidx.wear.compose.material.TimeText](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#TimeText(androidx.compose.ui.Modifier,androidx.wear.compose.material.TimeSource,androidx.compose.ui.text.TextStyle,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1,kotlin.Function0,kotlin.Function1)) | +| [androidx.wear.compose.material3.VerticalPagerScaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | New | + +And finally a list of some relevant components from Wear Compose Foundation +library: + +| Wear Compose Foundation 1.7.0-alpha03 | | +|---|---| +| [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | +| [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | +| [androidx.wear.compose.foundation.pager.VerticalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#VerticalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A vertically scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | +| [androidx.wear.compose.foundation.lazy.TransformingLazyColumn](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#TransformingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.TransformingLazyColumnState,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) | Can be used instead of [`ScalingLazyColumn`](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/lazy/package-summary#ScalingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.foundation.lazy.ScalingLazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.lazy.ScalingParams,androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType,androidx.wear.compose.foundation.lazy.AutoCenteringParams,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)) to add scroll transform effects to each item. | +| | | + +### Buttons + +Buttons in M3 are different from M2.5. The M2.5 Chip has been replaced by +Button. [`Button`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.String,kotlin.Boolean,androidx.compose.ui.graphics.Shape,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.wear.compose.material3.SurfaceTransformation,kotlin.Function1)) implementation provides default values for `Text` +`maxLines` and `textAlign`. Those default values can be overridden in the `Text` +element. + +### M2.5 + + import androidx.wear.compose.material.Chip + + //M2.5 Buttons + Chip(...) + CompactChip(...) + Button(...) + +### M3 + + +```kotlin +//M3 Buttons +Button(onClick = { }){} +CompactButton(onClick = { }){} +IconButton(onClick = { }){} +TextButton(onClick = { }){} +``` + +
+ +M3 also includes new button variations. Check them out on the [Compose Material +3 API reference overview](https://developer.android.com/jetpack/androidx/releases/wear-compose#wear_compose_version_15_2). + +M3 introduces a new button: [`EdgeButton`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#EdgeButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.wear.compose.material3.EdgeButtonSize,kotlin.Boolean,androidx.wear.compose.material3.ButtonColors,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)). `EdgeButton` is available in 4 +different sizes: extra small, small, medium, and large. `EdgeButton` +implementation provide a default value for `maxLines` depending on the size +which can be customized. + +If you are using `TransformingLazyColumn` or `ScalingLazyColumn`, pass the +`EdgeButton` into the `ScreenScaffold` so that it morphs, changing its shape +with scrolling, instead of adding an `EdgeButton` as the final list item. See +the following code to check how to use `EdgeButton` with `ScreenScaffold` and +`TransformingLazyColumn`. + + +```kotlin +val state = rememberTransformingLazyColumnState() +ScreenScaffold( + scrollState = state, + contentPadding = + rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader + ), + edgeButton = { + EdgeButton( + onClick = { } + ) { + Text(stringResource(R.string.show)) + } + } +){ contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding,){ + // additional code here + } +} +``` + +
+ +### Scaffold + +Scaffold in M3 is different from M2.5. In M3, `AppScaffold` and the new +`ScreenScaffold` composable have replaced Scaffold. `AppScaffold` and +`ScreenScaffold` lay out the structure of a screen and coordinate transitions of +the `ScrollIndicator` and `TimeText` components. + +`AppScaffold` allows static screen elements such as `TimeText` to remain visible +during in-app transitions such as swipe-to-dismiss. ​​It provides a slot for the +main application content, which will usually be supplied by a navigation +component such as `SwipeDismissableNavHost` + +You declare one `AppScaffold` for Activity and use a `ScreenScaffold` for each +Screen. +`AppScaffold` adds a default `TimeText`component to the screens. You can +override it if you want to customize it by using the `timeText` parameter. + +### M2.5 + + import androidx.wear.compose.material.Scaffold + + Scaffold {...} + +### M3 + + +```kotlin + AppScaffold { + val navController = rememberSwipeDismissableNavController() + SwipeDismissableNavHost( + navController = navController, + startDestination = "message_list" + ) { + composable("message_list") { + MessageList(onMessageClick = { id -> + navController.navigate("message_detail/$id") + }) + } + composable("message_detail/{id}") { + MessageDetail(id = it.arguments?.getString("id")!!) + } + } + } +} + +// Implementation of one of the screens in the navigation +@Composable +fun MessageDetail(id: String) { + // .. Screen level content goes here + val scrollState = rememberTransformingLazyColumnState() + + val padding = rememberResponsiveColumnPadding( + first = ColumnItemType.BodyText + ) + + ScreenScaffold( + scrollState = scrollState, + contentPadding = padding + ) { scaffoldPaddingValues -> + // Screen content goes here + // ... +``` + +
+ +> [!NOTE] +> **Note:** `AppScaffold` and `ScreenScaffold` from [Horologist](https://github.com/google/horologist) haven't been migrated to M3. To maintain correct scrolling behavior and `TimeText` elements, migrate to the `AppScaffold` and `ScreenScaffold` from M3. + +If you are using a `HorizontalPager` with [HorizontalPagerIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState,androidx.compose.ui.Modifier,androidx.wear.compose.material.PageIndicatorStyle,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape)), you +can migrate to `HorizontalPagerScaffold`. [`HorizontalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) is +placed within an `AppScaffold`. `AppScaffold` and `HorizontalPagerScaffold` lay +out the structure of a Pager and coordinate transitions of the +`HorizontalPageIndicator` and `TimeText` components. + +`HorizontalPagerScaffold` displays the `HorizontalPageIndicator` at the +center-end of the screen by default and coordinates showing and hiding +`TimeText` and `HorizontalPageIndicator` according to whether the `Pager` is +being paged, this is determined by the `PagerState`. + +There's also a new `AnimatedPage` component, which animates a page within a +Pager with a scaling and scrim effect based on its position. + + +```kotlin +AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + val columnState = rememberTransformingLazyColumnState() + val contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader, + last = ColumnItemType.BodyText, + ) + HorizontalPagerScaffold(pagerState = pagerState) { + HorizontalPager( + state = pagerState, + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold( + scrollState = columnState, + contentPadding = contentPadding + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Pager sample") + } + } + item { + if (page == 0) { + Text(text = "Page #$page. Swipe right") + } + else{ + Text(text = "Page #$page. Swipe left and right") + } + } + } + } + + } + } + } +} +``` + +
+ +Finally, M3 introduces a [`VerticalPagerScaffold`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,kotlin.Function1,androidx.compose.animation.core.AnimationSpec,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) which follows the same +pattern as the `HorizontalPagerScaffold`: + + +```kotlin +AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + + VerticalPagerScaffold(pagerState = pagerState) { + VerticalPager( + state = pagerState + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + ///... + } + } + } + } +} +``` + +
+ +### Placeholder + +There are some API changes between M2.5 and M3. +`Placeholder.PlaceholderDefaults` now provides two modifiers: + +- [`Modifier.placeholder`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholder(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)), which is drawn instead of content that is not yet loaded +- A placeholder shimmer effect [`Modifier.placeholderShimmer`](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary#(androidx.compose.ui.Modifier).placeholderShimmer(androidx.wear.compose.material3.PlaceholderState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color)) which provides a placeholder shimmer effect which runs in an animation loop while waiting for the data to load. + +See the following table for additional changes to the `Placeholder` component. + +| M2.5 | M3 | +|---|---| +| `PlaceholderState.startPlaceholderAnimation` | Has been removed | +| `PlaceholderState.placeholderProgression` | Has been removed | +| `PlaceholderState.isShowContent` | Has been renamed to `!PlaceholderState.isVisible` | +| `PlaceholderState.isWipeOff` | Has been removed | +| `PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush` | Has been removed | +| `PlaceholderDefaults.placeholderBackgroundBrush` | Has been removed | +| `PlaceholderDefaults.placeholderChipColors` | Has been removed | + +### SwipeDismissableNavHost + +`SwipeDismissableNavHost` is part of `wear.compose.navigation`. When this +component is used with M3, the M3 MaterialTheme updates the +`LocalSwipeToDismissBackgroundScrimColor` and +`LocalSwipeToDismissContentScrimColor`. + +### TransformingLazyColumn + +`TransformingLazyColumn` is part of `wear.compose.lazy.foundation` and adds +support for scaling and morphing animations on list items during scrolling , +enhancing the user experience. It is strongly recommended that apps migrate from +`ScalingLazyColumn` to `TransformingLazyColumn` + +Similarly to `ScalingLazyColumn`, it provides +`rememberTransformingLazyColumnState()` to create a +`TransformingLazyColumnState` that is remembered across compositions. + +For adding scaling and morphing animations, add the following to each list item: + +- `Modifier.transformedHeight`, which lets you calculate transformed height of the items using a `TransformationSpec`, you can use `rememberTransformationSpec()` unless you need further customization. +- A `SurfaceTransformation` + +To verify that the padding is correct at the top and bottom of the list, use the +`minimumVerticalContentPadding` modifier. + + +```kotlin +val columnState = rememberTransformingLazyColumnState() +val transformationSpec = rememberTransformationSpec() +ScreenScaffold( + scrollState = columnState +) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} +``` + +
+ +## Useful links + +To learn more about migrating from M2.5 to M3 in Compose, consult the following +additional resources. + +### Samples + +- [Wear OS samples on GitHub](https://github.com/android/wear-os-samples/) +- [Compose for Wear OS codelab](https://developer.android.com/codelabs/compose-for-wear-os#0) +- [Jetcaster sample](https://github.com/android/compose-samples/tree/main/Jetcaster) + +### API reference and source code + +- [Compose Material 3 API reference](https://developer.android.com/reference/kotlin/androidx/wear/compose/material3/package-summary) +- [Compose Material 3 samples in source code](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/) + +### Design + +- [Design guidance](https://developer.android.com/design/ui/wear/guides/get-started) \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 120a0d0..321f178 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-13' + last-updated: '2026-05-20' keywords: - Jetpack Compose Glimmer - audio glasses @@ -38,7 +38,7 @@ metadata: - Mobile project must target `compileSdk` 37 or higher. If the `compileSdk` is lower than 37, increase the `compileSdk` to 37. - Ensure you are using the latest library dependencies from [Create your first - activity for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). + activity for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). ## Core Constraints @@ -56,15 +56,15 @@ metadata: ## 1. Set up dependencies - **Setup Projected Activity:** First, you need to create a new projected activity for your app. If the project doesn't already have one, see [Create - your first activity for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. Ensure that you specify `xr_projected` for the `android:requiredDisplayCategory` attribute in app manifest to tell the system that this activity will use a projected context to access hardware from a connected device. + your first activity for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. Ensure that you specify `xr_projected` for the `android:requiredDisplayCategory` attribute in app manifest to tell the system that this activity will use a projected context to access hardware from a connected device. - **Mobile App Integration:** If the project contains an existing mobile app, you must create a new Glasses Activity dedicated to rendering Glimmer UI. For detailed configuration, heavily reference [Create your first activity - for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the display glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. + for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the display glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. - **UI Library:** Identify if the project has the `androidx.xr.glimmer:glimmer` library, if not it must be added to the project. See [Declaring Jetpack Compose Glimmer Dependencies](https://developer.android.com/jetpack/androidx/releases/xr-glimmer#declaring_dependencies) to fetch the latest dependency version. - **Theming:** All Glimmer components must be wrapped within the `GlimmerTheme` composable to ensure correct token resolution. - **Mandatory black background:** Display glasses use additive displays. Any non-black color in the background blocks the real world. **You must always** set a pure black background (`Modifier.background(Color.Black)`) on the root container of your Projected Activity. - **Font:** The default font is Google Sans Flex. Use `androidx.xr.glimmer.googlefonts` library with the default type styles unless otherwise specified. Use `createGoogleSansFlexTypography` to create a `Typography` instance with the Google Sans Flex configuration. Provide this `Typography` instance as normal through `GlimmerTheme`. Use [references/glimmersansflextypography-source.md](references/glimmersansflextypography-source.md) for configuration. - **Hardware Capabilities:** Different types of intelligent eyewear devices have different capabilities. To check for these at runtime, see the [Check - device capabilities at runtime for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md). + device capabilities at runtime for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities). - **Hardware Permissions:** To request hardware permissions like the microphone and camera, see the [Request hardware permissions for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md). - **Hardware Access:** To use the glasses camera, sensors, or access the phone's hardware, see the [Use a projected context to access hardware on @@ -334,4 +334,4 @@ component. ## 5. Integrate with system UI -- For a detailed breakdown of notifications on intelligent eyewear, see [Understand notification behavior for intelligent eyewear](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md) and learn how to [Start a glasses activity on display glasses from a notification](references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md). +- For a detailed breakdown of notifications on intelligent eyewear, see [Understand notification behavior for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior) and learn how to [Start a glasses activity on display glasses from a notification](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity). diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md index 67de706..ee543de 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md @@ -1,26 +1,28 @@ -Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \& +Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
After you've [requested and been granted the necessary permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions), your app -can access the AI glasses hardware. The key to accessing the glasses' hardware -(instead of the phone's hardware), is to use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context). +can access the hardware on the audio glasses or display glasses. The key to +accessing the glasses' hardware (instead of the phone's hardware), is to use a +[projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context). There are two primary ways to get a projected context, depending on where your code is executing: -## Get a projected context if your code is running in an AI glasses activity +## Get a projected context if your code is running in an projected activity -If your app's code is running from within your AI glasses activity, its own -activity context is already a projected context. In this scenario, calls made -within that activity can already access the glasses' hardware. +If your app's code is running from within your [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#activity-lifecycle), its own +activity context is already a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context). In this scenario, calls +made within that activity can already access the glasses' hardware. ## Get a projected context for code running in a phone app component -If a part of your app outside of your AI glasses activity (such as a phone +If a part of your app outside of your projected activity (such as a phone activity or a service) needs to access the glasses' hardware, it must explicitly obtain a projected context. To do this, use the -[`createProjectedDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) method: +[`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) method: ```kotlin @@ -43,8 +45,8 @@ private fun getGlassesContext(context: Context): Context? { Wrap the [`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)) call within the [`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)). While this method returns `true`, the projected context remains valid to the connected device, and your -phone app activity or service (such as a `CameraManager`) can access the -AI glasses hardware. +phone app activity or service (such as a `CameraManager`) can access the AI +glasses hardware. ### Clean up on disconnect @@ -57,26 +59,161 @@ context. ### Re-initialize on reconnect -When the AI glasses device reconnects, your app can obtain another projected -context instance using [`createProjectedDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)), and then +When the glasses reconnect, your app can obtain another projected +context instance using [`createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)), and then re-initialize any system services or resources using the new projected context. -## Access audio using bluetooth +## Record audio with the glasses' microphone -Currently, AI glasses connect to your phone as a standard Bluetooth audio -device. Both the headset and the A2DP (Advanced Audio Distribution Profile) -[profiles](https://developer.android.com/develop/connectivity/bluetooth/profiles) are supported. Using this approach lets any Android app that -supports audio input or output work on glasses, even if they were not -purposefully built to support glasses. In some cases, using Bluetooth might work -better for your app's use case as an alternative to accessing the glasses' -hardware using a projected context. +You can record audio from the glasses using two distinct methods: -As with any standard Bluetooth audio device, the permission to grant the -[`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio) permission is controlled by the phone and not the glasses. +- Use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#projected-context). +- Use Bluetooth Hands-Free Profile (HFP). -## Capture an image with the AI glasses' camera +### Choose a recording methods -To capture an image with the AI glasses' camera, set up and bind the CameraX's +The method you choose depends on whether you need high-fidelity, XR-specific +audio processing, or standard Bluetooth audio input. + +| Recording method | Microphone access | Common use case | +|---|---|---| +| Projected Context | Multiple microphones | Recording using a projected context lets your app access multiple microphones from the glasses and its specialized hardware features, such as: - XR-specific spatialization. - Advanced denoising. - Voice separation that distinguishes between wearer's and bystander's voices. - Maintain recording access in multidevice environments even when the glasses are not the active Bluetooth device. | +| Bluetooth HFP | Single microphone | Relies on the Bluetooth Hands-Free Profile (HFP) for immediate, out-of-the-box compatibility. In this mode, the glasses connect to the phone using standard Headset and Advanced Audio Distribution Profile (A2DP) [profiles](https://developer.android.com/develop/connectivity/bluetooth/profiles), functioning like a typical Bluetooth peripheral. If your app is already designed for standard Bluetooth recording, you can use this method to record audio from the glasses without integrating any XR-specific capabilities. | + +### Record audio using a projected context + +To record audio using a projected context, first request the required runtime +permissions, and then record the audio using the [`AudioRecord`](https://developer.android.com/reference/kotlin/android/media/AudioRecord) API, as +described in the following sections. + +#### Request runtime permissions + +To access multiple microphones on the glasses, you must request audio +permissions specifically for the projected device. The standard, phone-scoped +`RECORD_AUDIO` permission that a user has granted for your app on their mobile +device is insufficient. + +Follow these steps to request the permissions: + +1. [Declare the `RECORD_AUDIO` permission](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#declare-permissions) in your app's manifest file. +2. Request the projected-device-scoped permissions in one of the following + ways, depending on where your code is executing: + + - **Code executing from a projected activity** : Use the [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()). For more information on using this method, see the [register the permissions launcher](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#register) section and subsequent sections in the guide for requesting hardware permissions. + - **Code executing from a host phone activity** : Use [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1) and provide the device ID obtained from your `projectedDeviceContext`, as described in the [understand the permission request user flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow) section of the guide for requesting hardware permissions. + +#### Initialize AudioRecord with a projected context + +To ensure that audio is recorded from the glasses rather than the host phone, +you must associate the `AudioRecord` object with the projected device context. + +The following code uses the [`AudioRecord.Builder`](https://developer.android.com/reference/kotlin/android/media/AudioRecord.Builder) and passes the +`projectedDeviceContext` to the [`setContext`](https://developer.android.com/reference/kotlin/android/media/AudioRecord.Builder#setcontext) method: + + +```kotlin +// Initialize AudioRecord with projected device context +val audioRecord = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.CAMCORDER) + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + // pass in the projected device context + .setContext(projectedDeviceContext) + .build() + +audioRecord.startRecording() +``` + +
+ +##### Key points about the code + +- You can set the audio source to [`CAMCORDER`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#camcorder), + [`VOICE_RECOGNITION`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#voice_recognition), [`VOICE_COMMUNICATION`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#voice_communication), or + [`UNPROCESSED`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource#unprocessed) to tailor the audio processing to your specific use + case. + + For example, use `VOICE_COMMUNICATION` if your use case needs automatic + noise reduction. `VOICE_RECOGNITION` is processed with acoustic echo + cancellation (AEC). And if you need raw, unaltered audio, select + `UNPROCESSED` or `CAMCORDER`. +- To ensure compatibility with the glasses, the `audioFormat` object must + define a sample rate of 16kHz and a channel configuration of either mono or + stereo (using [`CHANNEL_IN_MONO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_mono) or [`CHANNEL_IN_STEREO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_stereo)). + +- While there is no fixed requirement for buffer size, [get the minimum buffer + size](https://developer.android.com/reference/kotlin/android/media/AudioRecord#getminbuffersize) to minimize perceived latency. + +> [!WARNING] +> **Preview:** Support for using the [`MediaRecorder`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder) API with a projected context will be available in an upcoming Android Beta release. + +##### Clean up after use + +When your app no longer needs the microphone, or when the activity is stopped, +call [`stop`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#stop) and [`release`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#release) on the `AudioRecord` object. + +##### Check for runtime permissions before recording + +Before calling [`startRecording`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#startrecording), [verify that the user has granted the +microphone permission for the glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#check-function) using the projected context. + +### Record audio using Bluetooth HFP + +To record audio using Bluetooth HFP, first request the required runtime +permissions, and then record the audio using the [`AudioManager`](https://developer.android.com/reference/kotlin/android/media/AudioManager) API, as +described in the following sections. + +#### Request permissions + +As with any standard Bluetooth audio device, the [`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio), +[`BLUETOOTH_CONNECT`](https://developer.android.com/reference/kotlin/android/Manifest.permission#bluetooth_connect), and other related permissions are controlled by the +phone and not the connected device (such as audio glasses or display glasses). + +Follow these steps to request the permissions: + +1. [Declare following permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#declare-permissions) in your app's manifest file: + + - [`RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio) + - [`BLUETOOTH_CONNECT`](https://developer.android.com/reference/kotlin/android/Manifest.permission#bluetooth_connect) + - [`MODIFY_AUDIO_SETTINGS`](https://developer.android.com/reference/kotlin/android/Manifest.permission#modify_audio_settings) +2. Request both the `RECORD_AUDIO` and `BLUETOOTH_CONNECT` permissions at + runtime using the [standard Android permission flow](https://developer.android.com/training/permissions/requesting). + +#### Use AudioManager to route audio + +After the user has granted your app the necessary runtime permissions, use the +[`AudioManager`](https://developer.android.com/reference/kotlin/android/media/AudioManager) API to set the communication device to +[`TYPE_BLUETOOTH_SCO`](https://developer.android.com/reference/kotlin/android/media/AudioDeviceInfo#type_bluetooth_sco) to route the audio through Bluetooth HFP. This +directs the system to retrieve audio from the Bluetooth peripheral. + + +```kotlin +val audioManager = context.getSystemService(AudioManager::class.java) ?: return +val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS) +val hfpDevice = devices.find { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } + +hfpDevice?.let { device -> + val audioRecord = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + .build() + + // Route recording to the Bluetooth device + audioRecord.setPreferredDevice(device) + audioManager.setCommunicationDevice(device) + + audioRecord.startRecording() +``` + +
+ +> [!NOTE] +> **Note:** To learn more about Bluetooth connectivity, refer to our [documentation](https://developer.android.com/develop/connectivity/bluetooth). + +## Capture an image with the glasses' camera + +To capture an image with the glasses' camera, set up and bind the CameraX's [`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture) [use case](https://developer.android.com/media/camera/camerax#ease-of-use) to the glasses' camera using the correct context for your app: @@ -156,20 +293,20 @@ private fun startCameraOnGlasses(activity: ComponentActivity) { ### Key points about the code - Obtains an instance of the [`ProcessCameraProvider`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider) using the [**projected device context**](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware-projected-context#phone-activity-service). -- Within the projected context's scope, the AI glasses' primary, outward-pointing camera maps to the `DEFAULT_BACK_CAMERA` when selecting a camera. +- Within the projected context's scope, the glasses' primary, outward-pointing camera maps to the `DEFAULT_BACK_CAMERA` when selecting a camera. - A pre-binding check uses [`cameraProvider.hasCamera(cameraSelector)`](https://developer.android.com/reference/kotlin/androidx/camera/lifecycle/ProcessCameraProvider#hasCamera(androidx.camera.core.CameraSelector)) to verify that the selected camera is available on the device before proceeding. - Uses **Camera2 Interop** with [`Camera2CameraInfo`](https://developer.android.com/reference/kotlin/androidx/camera/camera2/interop/Camera2CameraInfo) to read the underlying [`CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP`](https://developer.android.com/reference/kotlin/android/hardware/camera2/CameraCharacteristics#scaler_stream_configuration_map), which can be useful for advanced checks on supported resolutions. - A custom [`ResolutionSelector`](https://developer.android.com/reference/kotlin/androidx/camera/core/resolutionselector/ResolutionSelector) is built to precisely control the output image resolution for [`ImageCapture`](https://developer.android.com/reference/kotlin/androidx/camera/core/ImageCapture). - Creates an `ImageCapture` use case that is configured with a custom `ResolutionSelector`. - Binds the `ImageCapture` use case to the activity's lifecycle. This automatically manages the opening and closing of the camera based on the activity's state (for example, stopping the camera when the activity is paused). -After the AI glasses' camera is set up, you can capture an image with the +After the glasses' camera is set up, you can capture an image with the CameraX's `ImageCapture` class. Refer to the CameraX's documentation to learn -about using [`takePicture()`](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture) to [capture an image](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture). +about using [`takePicture`](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture) to [capture an image](https://developer.android.com/media/camera/camerax/take-photo#take_a_picture). -### Capture a video with the AI glasses' camera +### Capture a video with the glasses' camera -To capture a video instead of an image with the AI glasses' camera, replace the +To capture a video instead of an image with the glasses' camera, replace the `ImageCapture` components with the corresponding [`VideoCapture`](https://developer.android.com/reference/kotlin/androidx/camera/video/VideoCapture) components and modify the capture execution logic. @@ -179,7 +316,7 @@ For more information about the `VideoCapture` API and how to use it, see the [CameraX's video capture documentation](https://developer.android.com/media/camera/camerax/video-capture#using-videocapture-api). > [!IMPORTANT] -> **Important:** Battery power and thermal dissipation on AI glasses devices is often limited. When developing for glasses, pay close attention to the requested **output format** and **resolution**, as these can severely impact system power and temperature. +> **Important:** Battery power and thermal dissipation on glasses devices is often limited. When developing for glasses, pay close attention to the requested **output format** and **resolution**, as these can severely impact system power and temperature. The following table shows the recommended resolution and frame rate depending on your app's use case: @@ -190,11 +327,11 @@ your app's use case: | Computer Vision | 640 x 480 | 10 FPS | | AI Video Streaming | 640 x 480 | 1 FPS | -## Access a phone's hardware from an AI glasses activity +## Access a phone's hardware from a projected activity -An AI glasses activity can also access the phone's hardware (such as the camera -or microphone) by using [`createHostDeviceContext(context)`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the host -device's (phone) context: +An [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/support-different-types#activity-lifecycle) can also access the phone's hardware (such as the +camera or microphone) by using [`createHostDeviceContext(context)`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get +the host device's (phone) context: ```kotlin @@ -213,9 +350,9 @@ private fun getPhoneContext(activity: ComponentActivity): Context? {
When accessing hardware or resources that are specific to the host device -(phone) in a hybrid app (an app containing both mobile and AI glasses +(phone) in a hybrid app (an app containing both mobile and glasses experiences), you must explicitly select the correct context to make sure your app can access the correct hardware: -- Use the [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) context from the phone `Activity` or the [`ProjectedContext.createHostDeviceContext()`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the phone's context. -- Don't use [`getApplicationContext()`](https://developer.android.com/reference/kotlin/android/content/Context#getapplicationcontext) because the application context can incorrectly return the AI glasses' context if a glasses activity was the most-recently-launched component. \ No newline at end of file +- Use the [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) context from the phone `Activity` or the [`ProjectedContext.createHostDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createHostDeviceContext(android.content.Context)) to get the phone's context. +- Don't use [`getApplicationContext`](https://developer.android.com/reference/kotlin/android/content/Context#getapplicationcontext) because the application context can incorrectly return the glasses' context if a projected activity was the most-recently-launched component. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md deleted file mode 100644 index eea9a07..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/check-capabilities.md +++ /dev/null @@ -1,47 +0,0 @@ -Different types of AI glasses have different capabilities. After [planning how -you'll support different types of AI devices](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types), you can check for device -capabilities at runtime to provide the best experience for a user's device. - -## Check whether a device has a display - -Some AI glasses have a display where your app can show [UIs built with Jetpack -Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer). The following example shows how to check whether a glasses -device has a display: - - -```kotlin -// Check device capabilities -val projectedDeviceController = ProjectedDeviceController.create(this@GlassesMainActivity) -isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI) -``` - -
- -## React to display state changes - -On AI glasses with a display, the display can time out or the user can turn off -the display. To design activities that run whether the display is on or off, -use [`addPresentationModeChangedListener`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDisplayController#addPresentationModeChangedListener(java.util.concurrent.Executor,java.util.function.Consumer)) to be notified when the display -state changes. You can tune your activity for the appropriate amount of -audio information depending on display state. - - ProjectedDisplayController.create(activity).addPresentationModeChangedListener { - presentationModeFlags -> - - val areVisualsOff = !presentationModeFlags.hasPresentationMode(VISUALS_ON) - } - -> [!NOTE] -> **Note:** Monitor display state changes for the AI glasses directly within the [projected activity](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle). - -## Keep the display on - -On AI glasses with a display, you can request that the system keep the screen on -and prevent the screen from timing out using [`addLayoutParamsFlags`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDisplayController#addLayoutParamsFlags(kotlin.Int)). - -> [!NOTE] -> **Note:** Keeping the device's screen on can drain the battery quickly. Ordinarily, you should let the device turn the screen off if the user isn't interacting with it. If you do need to keep the screen on, do so for as short a time as possible. - - var projectedDisplayController = ProjectedDisplayController.create(activity) - - projectedDisplayController.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md deleted file mode 100644 index 3493977..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity.md +++ /dev/null @@ -1,276 +0,0 @@ -
- - -Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) - -
- -The AI glasses experience is built on the existing Android [`Activity` framework -API](https://developer.android.com/guide/components/activities/intro-activities) and [includes additional concepts](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle) to support the unique aspects of -AI glasses. Unlike XR headsets that run a full APK on the device, AI glasses use -a dedicated activity that runs within your phone's existing app. This activity -is projected from the host device to the AI glasses. - -To create your app's AI glasses experience, you extend your existing phone app -by creating a new projected [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) for AI glasses. This activity serves -as the main launch entry point for your app on AI glasses. This approach -simplifies development because you can share and reuse business logic between -your phone and AI glasses experiences. - -## Version compatibility - -Check the [Android SDK compatibility requirements](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#compatibility) for the Jetpack XR SDK. - -### Dependencies - -Add the following [library dependencies for AI glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#augmented): - -### Groovy - - dependencies { - implementation "androidx.xr.runtime:runtime:1.0.0-alpha13" - implementation "androidx.xr.glimmer:glimmer:1.0.0-alpha12" - implementation "androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha12" - implementation "androidx.xr.projected:projected:1.0.0-alpha07" - implementation "androidx.xr.arcore:arcore:1.0.0-alpha13" - } - -### Kotlin - - dependencies { - implementation("androidx.xr.runtime:runtime:1.0.0-alpha13") - implementation("androidx.xr.glimmer:glimmer:1.0.0-alpha12") - implementation("androidx.xr.glimmer:glimmer-google-fonts:1.0.0-alpha12") - implementation("androidx.xr.projected:projected:1.0.0-alpha07") - implementation("androidx.xr.arcore:arcore:1.0.0-alpha13") - } - -## Declare your activity in your app's manifest - -Just like other types of activities, you need to declare your activity in your -app's manifest file for the system to see and run it. - - - - - - - - - -### Key points about the code - -- Specifies `xr_projected` for the `android:requiredDisplayCategory` attribute to tell the system that this activity should use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context) to access hardware from a connected device. - -## Create your activity - -Next, you'll create a small activity that can display something on the AI -glasses whenever the display is turned on. - - -```kotlin -@OptIn(ExperimentalProjectedApi::class) -class GlassesMainActivity : ComponentActivity() { - - private var displayController: ProjectedDisplayController? = null - private var isVisualUiSupported by mutableStateOf(false) - private var areVisualsOn by mutableStateOf(true) - private var isPermissionDenied by mutableStateOf(false) - - // Register the permissions launcher using the ProjectedPermissionsResultContract. - private val requestPermissionLauncher: ActivityResultLauncher> = - registerForActivityResult(ProjectedPermissionsResultContract()) { results -> - if (results[Manifest.permission.CAMERA] == true) { - isPermissionDenied = false - initializeGlassesFeatures() - } else { - // Handle permission denial. - isPermissionDenied = true - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - displayController?.close() - displayController = null - } - }) - - if (hasCameraPermission()) { - initializeGlassesFeatures() - } else { - requestHardwarePermissions() - } - - setContent { - GlimmerTheme { - HomeScreen( - areVisualsOn = areVisualsOn, - isVisualUiSupported = isVisualUiSupported, - isPermissionDenied = isPermissionDenied, - onRetryPermission = { requestHardwarePermissions() }, - onClose = { finish() } - ) - } - } - } - - private fun initializeGlassesFeatures() { - lifecycleScope.launch { - // Check device capabilities - val projectedDeviceController = ProjectedDeviceController.create(this@GlassesMainActivity) - isVisualUiSupported = projectedDeviceController.capabilities.contains(CAPABILITY_VISUAL_UI) - - val controller = ProjectedDisplayController.create(this@GlassesMainActivity) - displayController = controller - val observer = GlassesLifecycleObserver( - context = this@GlassesMainActivity, - controller = controller, - onVisualsChanged = { visualsOn -> areVisualsOn = visualsOn } - ) - lifecycle.addObserver(observer) - } - } - - private fun hasCameraPermission(): Boolean { - return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == - PackageManager.PERMISSION_GRANTED - } - - private fun requestHardwarePermissions() { - val params = ProjectedPermissionsRequestParams( - permissions = listOf(Manifest.permission.CAMERA), - rationale = "Camera access is required to overlay digital content on your physical environment." - ) - requestPermissionLauncher.launch(listOf(params)) - } -} -``` - -
- -### Key points about the code - -- Opts in to using [opt-in APIs](https://developer.android.com/reference/kotlin/androidx/annotation/RequiresOptIn) from the [Jetpack Projected](https://developer.android.com/jetpack/androidx/releases/xr-projected) library. -- `GlassesMainActivity` extends [`ComponentActivity`](https://developer.android.com/reference/kotlin/androidx/activity/ComponentActivity), just as you would expect in mobile development. -- Because not all AI glasses have a display, checks whether the device has a display using [`ProjectedDeviceController`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedDeviceController). -- The `setContent` block within the `onCreate` function defines the root of the Composable UI tree for the activity. You'll implement the `HomeScreen` composable using [Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer). -- Initializes the UI during the activity's [`onCreate`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method (see [projected activity lifecycle](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#activity-lifecycle)). -- To prepare for camera-related features that [access the glasses hardware](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware-projected-context), [requests hardware permissions](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions) by registering a permissions launcher, defining the `hasCameraPermission` and `requestHardwarePermissions` functions, and checking whether permissions have been granted before calling `initializeGlassesFeatures`. - -## Implement the composable - -The activity that you created references a `HomeScreen` composable function that -you need to implement. The following code uses [Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer) to -define a composable that can display some text on the AI glasses' display: - - -```kotlin -@Composable -fun HomeScreen( - areVisualsOn: Boolean, - isVisualUiSupported: Boolean, - isPermissionDenied: Boolean, - onRetryPermission: () -> Unit, - onClose: () -> Unit, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .surface(focusable = false) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (isPermissionDenied) { - Card( - title = { Text("Permission Required") }, - action = { Button(onClick = onClose) { Text("Exit") } } - ) { - Text("Camera access is needed to use AI glasses features.") - Button(onClick = onRetryPermission) { Text("Retry") } - } - } else if (isVisualUiSupported) { - Card( - title = { Text("Android XR") }, - action = { - Button(onClick = onClose) { - Text("Close") - } - } - ) { - if (areVisualsOn) { - Text("Hello, AI Glasses!") - } else { - Text("Display is off. Audio guidance active.") - } - } - } else { - Text("Audio Guidance Mode Active") - } - } -} -``` - -
- -### Key points about the code - -- As you defined in your activity earlier, the `HomeScreen` function includes the composable content that the user sees when the AI glasses' display is on. -- The Jetpack Compose Glimmer [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.text.TextStyle)) component displays the text "Hello, AI Glasses!" to the glasses' display. -- The Jetpack Compose Glimmer `Button` closes the activity by calling `finish()` through `onClose` in the AI glasses activity. - -> [!NOTE] -> **Note:** For an example of developing for a displayless experience, see [Handle -> audio input using Automatic Speech Recognition](https://developer.android.com/develop/xr/jetpack-xr-sdk/asr) and [Handle audio input -> using Text to Speech](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts). - -## Check whether AI glasses are connected - -To determine whether a user's AI glasses are connected to their phone before -launching your activity, use the -[`ProjectedContext.isProjectedDeviceConnected`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#isProjectedDeviceConnected(android.content.Context,kotlin.coroutines.CoroutineContext)) method. This method -returns a `Flow` that your app can observe to get real-time updates on -the connection status. - -## Start your activity - -> [!WARNING] -> **Preview:** There is a known rendering issue that causes the AI glasses' display to briefly flash white when a projected activity starts. - -Now that you've created a basic activity, you can launch it onto your glasses. -To access the glasses' hardware, your app must start your activity with specific -options that tell the system to use a [projected context](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/support-different-types#projected-context), as shown in the -following code: - - -```kotlin -val options = ProjectedContext.createProjectedActivityOptions(context) -val intent = Intent(context, GlassesMainActivity::class.java) -context.startActivity(intent, options.toBundle()) -``` - -
- -The [`createProjectedActivityOptions`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedActivityOptions(android.content.Context)) method in [`ProjectedContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext) -generates the necessary options to start your activity in a projected context. -The `context` parameter can be a context from the phone or the glasses device. - -> [!WARNING] -> **Preview:** When your app launches a projected activity, it doesn't automatically turn on the AI glasses' display. We're planning to add this capability in the future. - -## Next steps - -Now that you've created your first activity for AI glasses, explore other ways -that you can extend its functionality: - -- [Handle audio output using Text to Speech](https://developer.android.com/develop/xr/jetpack-xr-sdk/tts) -- [Handle audio input using Automatic Speech Recognition](https://developer.android.com/develop/xr/jetpack-xr-sdk/asr) -- [Build UI with Jetpack Compose Glimmer](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer) -- [Access AI glasses' hardware](https://developer.android.com/develop/xr/jetpack-xr-sdk/access-hardware) \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md deleted file mode 100644 index 77d01dd..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior.md +++ /dev/null @@ -1,130 +0,0 @@ -
- - -Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) - -
- -AI glasses use the standard Android notification framework, so you can notify -users across different form factors using the same notification APIs. To promote -maximum compatibility and minimize development overhead, use the Android -[`NotificationCompat`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat) APIs. Android parses incoming notifications and adapts -their presentation based on the capabilities of the device. - -By following the [best practices for notifications](https://developer.android.com/develop/ui/views/notifications), your existing phone -notifications can be bridged to AI glasses with little to no additional -configuration. Read the information in the following sections to understand how -the system adapts notifications for AI glasses. - -## How the system determines whether to bridge a notification to AI glasses - -Android uses the incoming notification's [`Notification.Style`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Style) together with -several other criteria to determine whether or not to bridge the notification to -the user's AI glasses. - -### Supported notification styles - -AI glasses support a subset of Android `Notification.Style` classes. The -following notification styles are fully rendered on display AI glasses: - -- [Standard style (`NotificationCompat.Style`)](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Style) - - ![Standard style - notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/standard-notification.png) -- [`MessagingStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.MessagingStyle) - - ![MessagingStyle - notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/messagingstyle-notification.png) -- [`BigPictureStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.BigPictureStyle) - - ![BigPictureStyle - notification](https://developer.android.com/static/images/develop/xr/jetpack-xr-sdk/glimmer/bigpicturestyle-notification.png) -- [`ProgressStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.ProgressStyle) - -- [`MediaStyle`](https://developer.android.com/reference/kotlin/androidx/media/app/NotificationCompat.MediaStyle) - -- [`CallStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.CallStyle) (only when the notification [qualifies as a live - update](https://developer.android.com/develop/ui/views/notifications/live-update)) - -Other notification styles (such as [`InboxStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.InboxStyle)) aren't fully parsed and -rendered. For these styles, style-specific fields aren't rendered (such as the -[summary text](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.InboxStyle#setSummaryText(java.lang.CharSequence)) for `InboxStyle`). Instead, the system reverts to the -standard style and renders only common fields such as the [content title](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Builder#setContentTitle(java.lang.CharSequence)). - -> [!NOTE] -> **Note:** Custom notifications with `RemoteViews` aren't bridged to AI glasses. - -### Other required criteria for bridging - -Besides the notification's style, a notification must also meet the following -criteria to be bridged to AI glasses: - -1. The notification isn't subject to **any** of the following user-controlled - settings that would prevent its delivery: - - - **Glasses companion app settings**: - - - App-level toggle: By default, **app notifications are toggled off** - in the Glasses app to help users purposefully - decide which notifications are bridged to their AI glasses. - - This default behavior lets a user leave notifications - enabled on their phone for a certain app, but disable them for that - app on their AI glasses. To help a user decide whether to enable - notifications for your app in the Glasses app, - explain how notifications would improve their experience with your - app. - - **System-level notification settings on the user's phone**: - - - App-level toggle: If a user disables notifications entirely for an - app on the phone, no notifications for that app are bridged. - - - Notification channel settings: If a user disables notifications for - an [app-defined notification channel](https://developer.android.com/develop/ui/views/notifications/channels), no notifications for - that channel are bridged. - - - **System-level Do Not Disturb (DND) settings on the user's phone**: AI - glasses use the phone's DND settings. If the user's phone is in DND - mode, notifications are also suppressed on the user's glasses. - -2. The notification is assigned to a channel with [`IMPORTANCE_HIGH`](https://developer.android.com/reference/kotlin/android/app/NotificationManager#importance_high) or - [`IMPORTANCE_MAX`](https://developer.android.com/reference/kotlin/android/app/NotificationManager#importance_max). - -3. The [notification's title](https://developer.android.com/reference/kotlin/android/app/Notification#extra_title) isn't `null` or empty. - -4. The notification isn't marked with [`FLAG_LOCAL_ONLY`](https://developer.android.com/reference/kotlin/android/app/Notification#flag_local_only). If this flag is - set, the notification is restricted to the primary device. - -5. The notification isn't an [ongoing notification](https://developer.android.com/reference/kotlin/android/app/Notification.Builder#setongoing), such as a persistent - background task, unless it [qualifies as a Live Update notification](https://developer.android.com/develop/ui/views/notifications/live-update). - -## How Live Update notifications are bridged to AI glasses - -[Live Update notifications](https://developer.android.com/develop/ui/views/notifications/live-update) are a specialized class of notifications designed -for ongoing, user-initiated activities that require real-time monitoring, such -as rideshare ETAs, turn-by-turn navigation, or active calls. Unlike regular -notifications, live updates remain active to provide a continuous stream of -information that are surfaced prominently across the system UI. - -For display AI glasses, live updates are rendered in two primary locations: - -- **Home screen** : Live Update notifications appear on the Home canvas as [cards](https://developer.android.com/design/ui/ai-glasses/guides/components/cards). If multiple live updates are active, the system uses a [stack](https://developer.android.com/design/ui/ai-glasses/guides/components/stack) instead. -- **System bar** : When the user is inside another app or experience, live updates appear as status chips in the [system bar](https://developer.android.com/design/ui/ai-glasses/guides/surfaces/sysui#system-bar). These chips appear briefly whenever a status change occurs. If the display is asleep, a status chip automatically wakes the screen to signal a status change, so the user stays informed without manual interaction. - -If a live update notification uses a [supported notification style](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/behavior#notification-styles) for AI -glasses, it is fully parsed and rendered. Live Updates notifications that use -other notification styles (such as [`BigTextStyle`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.BigTextStyle)) aren't fully parsed, -and the system adapts them to a standard style notification instead. - -## Available notification actions for display AI glasses - -On display AI glasses, incoming notifications appear as [heads-up -notifications](https://developer.android.com/develop/ui/views/notifications#Heads-up) (HUNs). For `MessagingStyle` notifications, your app can use -a [direct reply](https://developer.android.com/develop/ui/views/notifications/build-notification#reply-action) action. Users can tap to expand for more details and reply. -For all other notification styles, the only available option is the -system-provided clear action. - -When using direct reply, users can reply with the voice or select from a smart -reply list by scrolling forward. For smart replies, you can use our [on-device -AI](https://developers.google.com/ml-kit/language/smart-reply/android) -to suggest short, relevant replies. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md deleted file mode 100644 index 45fe05c..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/ai-glasses/notifications/start-activity.md +++ /dev/null @@ -1,77 +0,0 @@ -
- - -Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) - -
- -You can specify a different [`PendingIntent`](https://developer.android.com/reference/kotlin/android/app/PendingIntent) that is invoked when a -notification is tapped *on the glasses display* . This `PendingIntent` is -distinct from the default intent set for the phone using -[`setContentIntent`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.Builder#setContentIntent(android.app.PendingIntent)). For example, when the user taps on the notification on -the display AI glasses, a specific glasses activity launches on the display AI -glasses. - -To add glasses-specific behaviors, use [`ProjectedExtender`](https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.ProjectedExtender). This API lets -you to customize the notification's behavior on the glasses without affecting -how it appears on the phone. - -### Kotlin - - // Intent to fire when tapped on the phone - val phoneIntent = Intent(this, MyPhoneActivity::class.java) - val phonePendingIntent = PendingIntent.getActivity( - this, 0, phoneIntent, PendingIntent.FLAG_IMMUTABLE - ) - - // Intent to fire when tapped on the glasses display - val glassesIntent = Intent(this, MyGlassesActivity::class.java) - val glassesPendingIntent = PendingIntent.getActivity( - this, 1, glassesIntent, PendingIntent.FLAG_IMMUTABLE - ) - - // Create the base notification - val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply { - setSmallIcon(R.drawable.ic_notification) - setContentTitle("Navigation in Progress") - setContentText("Tap to see details") - // Default intent for phone - setContentIntent(phonePendingIntent) - - // Create and apply the glasses extender - val projectedExtender = ProjectedExtender().setContentIntent(glassesPendingIntent) - - extend(projectedExtender) - } - - // Issue the notification - notificationManager.notify(NOTIFICATION_ID, builder.build()) - -### Java - - // Intent to fire when tapped on the phone - Intent phoneIntent = new Intent(this, MyPhoneActivity.class); - PendingIntent phonePendingIntent = - PendingIntent.getActivity(this, 0, phoneIntent, PendingIntent.FLAG_IMMUTABLE); - - // Intent to fire when tapped on the glasses display - Intent glassesIntent = new Intent(this, MyGlassesActivity.class); - PendingIntent glassesPendingIntent = - PendingIntent.getActivity(this, 1, glassesIntent, PendingIntent.FLAG_IMMUTABLE); - - // Create the base notification - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("New Update") - .setContentText("Something important happened.") - // Default intent for phone - .setContentIntent(phonePendingIntent); - - // Create and apply the Glasses extender - ProjectedExtender projectedExtender = new ProjectedExtender() - // glasses-specific intent - .setContentIntent(glassesPendingIntent); - builder.extend(projectedExtender); - - // Issue the notification - notificationManager.notify(NOTIFICATION_ID, builder.build()); \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md index b19399d..faf2887 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md @@ -1,65 +1,100 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
-In Jetpack Compose Glimmer, the [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.xr.glimmer.ButtonSize,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) component is an interactive -component that's optimized for AI glasses input, offering clear visual feedback -for their unfocused, focused, and pressed states to guide user actions. -![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons.png) **Figure 1.** An example of some different styles of buttons in Jetpack Compose Glimmer. +In Jetpack Compose Glimmer, a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable) is an interactive component that's +optimized for display glasses input, offering clear visual feedback through its +states to guide user actions. -## Example: Button variations +Buttons are built on the Jetpack Compose Glimmer [surface system](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/surface), which +automatically handles physical properties like borders and depth. - @Composable - fun GlimmerButtonExample() { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - // Basic Button - Button(onClick = { /* Do something */ }) { - Text("Test Button 1") - } +The standard button contains a text label and optional icons. You can use it for +primary or secondary actions. There are also specialized buttons, such as [icon +buttons](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icon-buttons) and [toggle buttons](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/toggle-buttons), which are defined as separate components in +Jetpack Compose Glimmer. - // Button with a leading icon - Button( - onClick = { /* Do something */ }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Favorite icon" - ) - } - ) { - Text("Test Button 2") - } +### Default - // Button with leading and trailing icons - Button( - onClick = { /* Do something */ }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Favorite icon" - ) - }, - trailingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_favorite), - contentDescription = "Favorite icon" - ) - } - ) { - Text("Test Button 3") - } - } +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons_anatomy_default.png) An example of some different styles of buttons in Jetpack Compose Glimmer. These examples show default, medium-sized buttons with three button states: Enabled (1), Focused (2), and Pressed (3). + +### Large + +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_buttons_large.png) An example of some different styles of buttons in Jetpack Compose Glimmer. These examples show large-sized buttons with three button states: Enabled (1), Focused (2), and Pressed (3). + +## Anatomy + +A button consists of a container and a label, with optional leading and trailing +icons. + +| Part | Description | +|---|---| +| Container | The background surface of the button. | +| Label | The text describing the action. | +| Icon (optional) | Leading or trailing visual indicators. | + +## Sizes + +Jetpack Compose Glimmer buttons support two size variants. These affect the +minimum height and internal padding. + +| Size | Minimum height | Default usage | +|---|---|---| +| Medium | 48.dp | Standard actions and lists (default). | +| Large | 72.dp | High-emphasis actions or primary screen entry points. | + +## States + +Buttons in Jetpack Compose Glimmer change their appearance to communicate their +state. + +- **Enabled**: The default state for an interactive button. +- **Focused** : When focused, the button applies a [`GlimmerTheme.depthEffectLevels.level1`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/DepthEffectLevels#level1()) and a focused border highlight. +- **Pressed**: When activated, a semi-transparent white overlay is applied to the surface. +- **Disabled**: The button doesn't respond to input and its visual appearance is adjusted. + +## Button defaults + +The following defaults apply to standard buttons: + +- By default, buttons use [`GlimmerTheme.typography.bodySmall`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodySmall()). Make sure that text within buttons is concise and clearly describes the action. +- The default shape for a button is [`GlimmerTheme.shapes.large`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Shapes#large()). This consistent rounding helps users identify buttons across the display glasses interface. + +> [!NOTE] +> **Note:** Any modifier passed to the `Button` composable is applied to the outer layout. While [`ButtonSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/ButtonSize) sets the default minimum height, you can also apply custom size modifiers to control the button's final layout, such as [`Modifier.fillMaxWidth`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/fillMaxWidth.modifier). + +## Example: Button with text + +The following code creates a standard button with text: + + +```kotlin +@Composable +fun ButtonSample() { + Button(onClick = {}) { Text("Send") } +} +``` + +
+ +## Example: Buttons with leading and trailing icons + +You can also add icons to the start (using `leadingIcon`) or end (using +`trailingIcon`) of the text to provide additional context. + +The following code creates a button with a leading icon: + + +```kotlin +@Composable +fun ButtonWithLeadingIconSample() { + Button(onClick = {}, leadingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") } +} +``` -### Key points about the code - -- The button icons source local XML vector drawables (`R.drawable.ic_favorite`) using [`painterResource`](https://developer.android.com/reference/kotlin/androidx/compose/ui/res/painterResource.composable#painterResource(kotlin.Int)), replacing the `Icons.Default` library dependency for optimized asset loading. -- The `leadingIcon` and `trailingIcon` parameters are utilized to inject icon Composables into the button layout, demonstrating Jetpack Compose Glimmer's support for flexible icon positioning. -- The buttons use the default sizing configuration, which automatically manages internal padding and text scaling to align with standard Jetpack Compose Glimmer design specifications without explicit size modifiers. \ No newline at end of file +
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md index 90e40e9..8c9b455 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md @@ -1,81 +1,97 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
-In Jetpack Compose Glimmer, the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) component is designed to group and -display related information in a single unit. Cards are highly adaptable, -supporting combinations of main content, optional titles and subtitles, and -leading or trailing icons. Cards fill the maximum full width of the AI glasses -display by default, are focusable, and can also be made clickable. +In Jetpack Compose Glimmer, the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable) component serves as the primary +container for related content, creating a clear visual boundary for digestible +units of information. Cards are highly adaptable, supporting combinations of +main content, optional titles, subtitles, and leading or trailing icons. Cards +fill the maximum available width by default, are focusable, and you can also +make them clickable. Cards support a vertical arrangement where the header image +is top-most, followed by the title, subtitle, and body content. + +Cards are built on the Jetpack Compose Glimmer [surface system](https://developer.android.com/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/surface), so they +inherit physical properties like depth effects, clipping, and consistent border +highlights. ![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_cards.png) **Figure 1.** An example of some different styles of cards in Jetpack Compose Glimmer. -## Card Anatomy and Slots +## Anatomy and slots -A Jetpack Compose Glimmer Card is built from several specialized elements, -allowing you to customize its content and layout. +A Jetpack Compose Glimmer Card is built from several specialized elements that +let you customize its content and layout. -- **Header**: The top section of the card, designed to hold an image. +| Slot | Description | +|---|---| +| Header | The top section of the card, designed to hold an image. | +| Title and subtitle | These text fields provide the main and secondary labels for the card. The title is placed above the subtitle. | +| Leading icon | An optional icon (typically an [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable)) that appears at the beginning of the card's content area. | +| Trailing icon | An optional icon that appears at the end of the card's content area. | +| Action | A slot for a primary interactive element, such as a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable). | +| Main content | The core body of the card for primary [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable) or content. | -- **Title and Subtitle**: These text fields provide the main and secondary - labels for the card. +## Card defaults -- **Leading Icon**: An optional icon that appears at the beginning of the - card's content area. +The following defaults apply to cards: -- **Trailing Icon**: An optional icon that appears at the end of the card's - content area. +- **Width**: Cards fill the maximum available width of the display to optimize legibility on display glasses. +- **Minimum height** : `80.dp` +- **Text vertical spacing** : `3.dp` +- **Header shape** : Uses [`RoundedCornerShape`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/shape/package-summary#RoundedCornerShape(androidx.compose.ui.unit.Dp)) with `24.dp` corners +- **Content padding** : Defaults to [`GlimmerTheme.componentSpacingValues.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/ComponentSpacingValues#medium()). This affects the outermost padding around header images and the content container. +- **Shape** : Defaults to [`GlimmerTheme.shapes.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Shapes#medium()). +- **Text rendering** : Uses the default values from + [`GlimmerTheme.typography`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/GlimmerTheme#typography()) for the following slots: -- **Action**: A slot for a primary interactive element, such as a Button. This - allows users to perform an action directly from the card. The slot is - available on a separate overload of the Card composable. + - Title: [`bodyMedium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodyMedium()) + - Subtitle: [`caption`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#caption()) + - Main content: [`bodySmall`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodySmall()) -- **Main Content**: The core body of the card, where you place the primary - Text or other content. +## Example: Basic card -## Basic example: Create a basic card +The following code creates a basic card: -You can create a very basic card with very little code: +```kotlin +@Composable +fun CardSample() { Card { Text("This is a card") } +} +``` -## Detailed example: Display a complex card +
-The following code shows how to use multiple slots together to build a card. It -also shows how to pair a [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) with a [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)). +## Example: Complex card with multiple slots - @Composable - fun SampleGlimmerCard() { - val myHeaderImage = painterResource(id = R.drawable.header_image) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - TitleChip { Text("Title Chip") } - Spacer(Modifier.height(TitleChipDefaults.AssociatedContentSpacing)) - Card( - action = { - Button(onClick = {}) { - Text("Action Button") - } - }, - header = { - Image( - painter = myHeaderImage, - contentDescription = "Header image for the card", - contentScale = ContentScale.FillWidth - ) - }, - title = { Text("Card Title") }, - subtitle = { Text("Card Subtitle") }, - leadingIcon = { FavoriteIcon, "Localized description" }, - trailingIcon = { FavoriteIcon, "Localized description" } - ) { - Text("A Jetpack Compose Glimmer Card using all available slots") +The following code shows how to use multiple slots together to build a card. + + +```kotlin +@Composable +fun CardWithTitleAndLeadingIconAndHeaderAndAction() { + Card( + action = { + Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") } - } + }, + title = { Text("Title") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + header = { + Image(MyHeaderImage, "Localized description", contentScale = ContentScale.FillWidth) + }, + ) { + Text("This is a card with a title, leading icon, header image, and action") } +} +``` + +
### Key points about the code -- Shows how to utilize various card elements, such as [`header`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`title`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`subtitle`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), [`leadingIcon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), and [`action`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)), to customize the card's content and structure. -- Shows an example of how to place a [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) and use a [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)). \ No newline at end of file +- Shows how to utilize various card elements, such as [`header`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), [`title`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), [`leadingIcon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), and [`action`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable), to customize the card's content and structure. +- Uses the standard [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function0)) overload with an action because only the card (or its internal action) needs to be focusable. If you need to make the entire card surface interactive, use the [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable#Card(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0)) overload with `onClick` instead. +- This card uses an action slot, so the card surface isn't focusable, and focus is directed to the action button instead. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md index 90a80b4..23519fd 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md @@ -1,20 +1,24 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
All Jetpack Compose Glimmer components are designed to work with standard input -methods similar to phones, such as a tap or swipe on the AI glasses' touchpad, -while also being receptive to input commands that are specific to AI glasses -hardware, such as [camera and display buttons](https://developer.android.com/design/ui/ai-glasses/guides/interaction/inputs). Jetpack Compose Glimmer -components automatically handle the necessary input events. For custom -components, you can utilize existing Compose APIs like -[`Modifier.draggable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)) or [`Modifier.scrollable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)) to implement specific -interaction behaviors. +methods, such as a tap or swipe on the glasses' touchpad, while also being +receptive to lower-level input commands that are specific to the hardware on +display glasses. Jetpack Compose Glimmer components automatically handle the +necessary input events. -On AI glasses with a display, pointer input can affect focus: +For standard actions like scroll and drag, use the Jetpack Compose Glimmer +components to promote a consistent experience. However, for custom components or +bespoke interaction behaviors, you can use existing Compose APIs like +[`Modifier.draggable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)) or [`Modifier.scrollable`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).scrollable(androidx.compose.foundation.gestures.ScrollableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.gestures.FlingBehavior,androidx.compose.foundation.interaction.MutableInteractionSource)). + +## Pointer input and focus + +On display glasses, pointer input can affect focus: - **Tap**: Direct interaction for activating element. Focus moves to an element when a user interacts with it. - **Swipe**: Used for navigation and scrolling. Unhandled swipe gestures automatically translate into focus movements, enabling seamless UI navigation without direct pointer input. @@ -28,10 +32,14 @@ On AI glasses with a display, pointer input can affect focus: > > Using a feature of Jetpack Compose, the system can automatically set the initial > focus to the very-first focusable element when the screen loads, which is often -> the top-left item on the screen. This feature is still in development and isn't -> enabled by default. To activate this feature, set the -> `isInitialFocusOnFocusableAvailable` flag to `true` in your activity's -> [`onCreate()`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method. +> the top-left item on the screen. +> +> This feature is still in development and isn't enabled by default. To activate +> this feature, set the `isInitialFocusOnFocusableAvailable` flag to `true` in +> your activity's [`onCreate()`](https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle)) method. +> +> import androidx.compose.ui.ExperimentalComposeUiApi +> import androidx.compose.ui.ComposeUiFlags > > class GlassesActivityExample : ComponentActivity() { > override fun onCreate(savedInstanceState: Bundle?) { @@ -68,6 +76,7 @@ To change the initially-focused item, you can add a top-level Modifier.focusProperties { onEnter = { initialFocus.requestFocus() + // Prevent focus from exiting the group cancelFocusChange() } } @@ -89,10 +98,10 @@ clear visual feedback during user interaction. - **Default** : The button's background color is derived from [`GlimmerTheme.colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()), its main content calculates the content - color of that surface, and icons are [`GlimmerTheme.colors.primary`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#primary()). + color of that surface. - **Focused**: The border width is increased to communicate focus. - **Focused + Pressed** : The background is set to - `GlimmerTheme.colors.surface` at full opacity to communicate its selected - state. \ No newline at end of file + `GlimmerTheme.colors.surface` at increased opacity to communicate its + selected state. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md index 94dcb98..75503d4 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md @@ -1,40 +1,95 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
-In Jetpack Compose Glimmer, the [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable#Icon(androidx.compose.ui.graphics.ImageBitmap,kotlin.String,androidx.compose.ui.Modifier)) component is specifically designed -for rendering single-color icons. `Icon` can accept [`ImageVector`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector), -[`ImageBitmap`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ImageBitmap), or [`Painter`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter) as its source. `Icon`, similar to -[`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.ui.text.TextStyle)), can intelligently apply a tint based on the surrounding UI theme. -While it defaults to a size provided by the [`LocalIconSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#LocalIconSize()), you can also -set custom icon sizes. +In Jetpack Compose Glimmer, the [`Icon`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Icon.composable) component is a UI element for +rendering single-color icons. Icons intelligently handle tinting and scaling so +that they remain legible and visually consistent with the [`GlimmerTheme`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/GlimmerTheme.composable). -## Example: Create a box with a large star icon +## Sizes - @Composable - fun GlimmerIconSample() { - GlimmerTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - painter = painterResource(id = R.drawable.ic_star), - contentDescription = "A star icon from Google Symbols", - modifier = Modifier.size(GlimmerTheme.iconSizes.large), - tint = GlimmerTheme.colors.primary +While icons default to the size provided by [`LocalIconSize`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#LocalIconSize()), you can also +use the three icon sizes provided to set an specific size. These sizes are also +used by default for the following contexts: + +| Size token | Default usage | +|---|---| +| `small` | For standard list items or small chips. | +| `medium` | For standalone icons and title chips. | +| `large` | For high-emphasis components like cards. | + +## Icon sources + +Icons can accept [`ImageVector`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector), [`ImageBitmap`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ImageBitmap), or [`Painter`](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter) as +their source. When defining your own icons, use `ImageVector` where possible to +promote sharp rendering at any scale on display glasses. + +## Color and Tinting + +- **Automatic tint** : The icon resolves its color based on the `LocalContentColor` provided by the parent surface `LocalContentColor` provided by the parent surface, such as a [`surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/surface.composable) or [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable). +- **Manual Tinting** : Use the `tint` parameter to apply a specific color. +- **Multicolored Assets** : For icons that should not be tinted (like multicolored brand logos), set `tint = Color.Unspecified`. +- **Generic Images** : For photographs or generic images that don't follow icon sizing and tinting rules, use the standard [`androidx.compose.foundation.Image`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/Image.composable) instead. + +## Example: Basic icon within a surface + +The following code creates an icon placed inside a circular surface, utilizing +the theme's primary color: + + +```kotlin +@Composable +fun IconSampleUsage() { + GlimmerLazyColumn( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { IconSizesSample() } + item { + Icon( + FavoriteIcon, + "Localized description", + Modifier.surface( + shape = CircleShape, + color = GlimmerTheme.colors.primary, + border = null, ) - } - } + .padding(12.dp), + ) } } +} +``` + +
+ +## Example: Icons with different sizes + +The following code demonstrates the different icon sizes: + + +```kotlin +@Composable +fun IconSizesSample() { + val iconSizes = GlimmerTheme.iconSizes + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.small)) + // medium is also the default size, defining explicitly for clarity + Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.medium)) + Icon(FavoriteIcon, "Localized description", Modifier.size(iconSizes.large)) + } +} +``` + +
### Key points about the code -- The icon's source loads a local XML vector drawable (`R.drawable.ic_star`) using [`painterResource`](https://developer.android.com/reference/kotlin/androidx/compose/ui/res/painterResource.composable#painterResource(kotlin.Int)), demonstrating the recommended approach for integrating icons into a Jetpack Compose Glimmer UI without external library overhead. -- The icon's size is customized by setting [`GlimmerTheme.iconSizes.large`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes#large()) with a modifier, demonstrating how to override Jetpack Compose Glimmer's predefined sizing. -- The icon's tint color is customized by setting [`GlimmerTheme.colors.primary`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#primary()) using the tint parameter, applying single-color icon tinting for visual consistency. \ No newline at end of file +- Each icon's size is customized using [`GlimmerTheme.iconSizes`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes) with a modifier. For icons, the default value is [`GlimmerTheme.iconSizes.medium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/IconSizes#medium()). Use these sizes instead of hard-coding values to maintain consistency across your UI. +- Provides a localized `contentDescription` for each icon. Always provide these descriptions unless the icon is purely decorative. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md index 3b2b89f..71ab764 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md @@ -1,44 +1,68 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
-In Jetpack Compose Glimmer, the [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/package-summary#Text(kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.text.style.TextAlign,androidx.compose.ui.unit.TextUnit,androidx.compose.ui.text.style.TextOverflow,kotlin.Boolean,kotlin.Int,kotlin.Int,kotlin.Function1,androidx.compose.foundation.text.TextAutoSize,androidx.compose.ui.text.TextStyle)) component builds upon the basic text -and lets you set various text properties like color, font size, font style, font -weight, font family, letter spacing, and text alignment. The Jetpack Compose -Glimmer `Text` component is unique in that it intelligently manages color +In Jetpack Compose Glimmer, the [`Text`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Text.composable) component lets you set various text +properties like color, font size, font style, font weight, font family, letter +spacing, and text alignment. + +Jetpack Compose Glimmer Text is unique because it intelligently manages color matching. For example, if no color override is specified, the text defaults to -the content color provided by the nearest surface in the UI hierarchy. +the content color provided by the nearest surface that it's sitting on. ## Example: Create a text heading in a box @Composable - fun GlimmerStyleSample() { - GlimmerTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = "This is a sample heading", - color = GlimmerTheme.colors.secondary - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle Click */ }) { - Text(text = "Sample Button") - } - } - } - } + fun TextSample() { + Text( + text = "This is a sample heading", + style = GlimmerTheme.typography.titleLarge ) } ### Key points about the code -- The [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.xr.glimmer.ButtonSize,kotlin.Function0,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function1)) composable is automatically interactable, has a - [`Colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()) background, and the text is automatically set to: +- Because no color is specified, this text looks at the nearest surface to pick the best readable color (usually white). - - style = [`GlimmerTheme.typography.bodyMedium`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#bodyMedium()) - - color = [`GlimmerTheme.Colors.surface`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Colors#surface()) \ No newline at end of file +## Sizing + +Typography scale in Jetpack Compose Glimmer is significantly larger than +standard mobile Material Design. Styles like `TitleLarge` and `BodyLarge` are +both `30.sp`, and the Caption is `18.sp`: + +| Style | Size (sp) | Weight | Line Height | +|---|---|---|---| +| titleLarge | 30 | 750 | 36.sp | +| titleMedium | 24 | 750 | 28.sp | +| titleSmall | 20 | 750 | 28.sp | +| bodyLarge | 30 | 520 | 36.sp | +| bodyMedium | 24 | 520 | 36.sp | +| bodySmall | 20 | 520 | 28.sp | +| caption | 18 | 650 | 24.sp | + +## Use Google Sans Flex + +[Google Sans Flex](https://fonts.google.com/specimen/Google+Sans+Flex?query=google+sans&preview.script=Latn) is a variable font specifically chosen for AI +glasses that is provided as part of the Jetpack Compose Glimmer. The font's +rounded corners and adjustable axes allow for ideal optical sizing, ensuring +that text remains glanceable and legible. If possible, to improve consistency +for users between your app and the system, use Google Sans Flex for all text +displayed on display glasses. + +To use Google Sans Flex, [add the `glimmer-google-fonts` library to your app's +dependencies](https://developer.android.com/develop/xr/jetpack-xr-sdk/set-up-sdk#augmented), then apply the font globally to the `GlimmerTheme`: + + +```kotlin +@Composable +fun GoogleSansFlexTypographySample() { + val typography = createGoogleSansFlexTypography() + GlimmerTheme(typography = typography) { + Text("Hello World", style = GlimmerTheme.typography.titleLarge) + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md index 7a83fe3..c700326 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md @@ -1,52 +1,63 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
-In Jetpack Compose Glimmer, the [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable#TitleChip(androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.foundation.BorderStroke,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1)) component is designed to -provide brief, non-interactive label for associated content, such as a Card. Use -title chips to display concise information like a short title, a name, or a -status. Since title chips are not focusable or interactive, they serve a purely -informational role within the a Jetpack Compose Glimmer UI. For example, you -might provide a title chip labeled "Ingredients" next to a scrollable list of -ingredients. -![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_titlechip_anatomy.png) **Figure 1.** An example of some different styles of title chips in Jetpack Compose Glimmer. +In Jetpack Compose Glimmer, the [`TitleChip`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChip.composable) component is a non-interactive +component that provides brief context or labeling for associated content, such +as a [`Card`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Card.composable) or a [`VerticalList`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/list/VerticalList.composable). -Default title chip and Sticky title chip shown. Sticky title chips are displayed -with an outline. +Use title chips for concise information like a short title, a category name, or +a status indicator. Normally, you should place title chips above the content +they describe to establish a clear structural relationship. +![](https://developer.android.com/static/images/design/ui/glasses/guides/glasses_components_titlechip_anatomy.png) **Figure 1.** An example of a default style title chip and sticky title chip in Jetpack Compose Glimmer. Each title chip has a label (1) and an optional leading icon or entity (2). -1. Title chips label -2. Optional leading icon or entity +## Basic example: Create a short title chip -## Basic example: Display a short title chip +The following code creates a basic title chip: -You can create a short title chip with very little code: +```kotlin +@Composable +fun TitleChipSample() { TitleChip { Text("Messages") } +} +``` -## Detailed example: Display a title chip with a card +
+ +## Example: Create a title chip with a card To use a title chip with another component, place the title chip -[`TitleChipDefaults.AssociatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#AssociatedContentSpacing()) above the other component in -the composable. The following code shows how to use a title chip with a card: +[`TitleChipDefaults.associatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#associatedContentSpacing()) above the other component in +the composable. The following code creates a title chip with a card: - @Composable - fun TitleChipExample() { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - TitleChip { Text("Title Chip") } - Spacer(Modifier.height(TitleChipDefaults.AssociatedContentSpacing)) - Card( - title = { Text("Title") }, - subtitle = { Text("Subtitle") }, - leadingIcon = { Icon(FavoriteIcon, "Localized description") }, - ) { - Text("Card Content") - } + +```kotlin +@Composable +fun TitleChipWithCardSample() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TitleChip { Text("Title Chip") } + Spacer(Modifier.height(TitleChipDefaults.associatedContentSpacing)) + Card( + title = { Text("Title") }, + subtitle = { Text("Subtitle") }, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Card Content") } } +} +``` + +
### Key points about the code -- The [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)) has a fixed height to provide the correct vertical spacing, defined by [`TitleChipDefaults.AssociatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#AssociatedContentSpacing()), between the two components. \ No newline at end of file +- The title chip is centered horizontally, which is the usual alignment for a title chip placed above a card. +- The [`Spacer`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Spacer.composable#Spacer(androidx.compose.ui.Modifier)) has a fixed height to provide the right amount of vertical spacing between the two components. This is defined using [`TitleChipDefaults.associatedContentSpacing`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/TitleChipDefaults#associatedContentSpacing()). +- Uses an optional `leadingIcon` to add additional visual context before the text content. +- The title chip automatically uses the [`caption`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Typography#caption()) style for its text. +- The title chip isn't interactive. If you need a clickable label, use a [`Button`](https://developer.android.com/reference/kotlin/androidx/xr/glimmer/Button.composable) or another interactive component. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md index 450dd4b..162b00c 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md @@ -1,12 +1,14 @@
-Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) AI Glasses [](https://developer.android.com/develop/xr/devices#ai-glasses) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) +Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \& +Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices)
Just like on a phone, accessing sensitive hardware like the camera and -microphone on AI glasses requires explicit user consent. These are considered +microphone on audio glasses and display glasses requires explicit user consent. +These are considered **glasses-specific permissions**, and your app must request them at runtime, even if it already has the corresponding permissions on the phone. @@ -14,8 +16,8 @@ even if it already has the corresponding permissions on the phone. Before requesting permissions, you must [declare them in your app's manifest](https://developer.android.com/training/permissions/declaring) file using the [``](https://developer.android.com/guide/topics/manifest/uses-permission-element) element. This declaration remains the -same whether the permission is for a phone or an AI-glasses-specific feature, -but you must still explicitly request it for glasses-specific hardware or +same whether the permission is for a phone or a glasses-specific feature, but +you must still explicitly request it for glasses-specific hardware or functionality. @@ -29,10 +31,9 @@ functionality. ## Register the permissions launcher -To request permissions for AI glasses, first you use the -[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the -[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method to register the permissions -launcher. +To request permissions for audio glasses and display glasses, first you use the +[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) +method to register the permissions launcher. ```kotlin @@ -115,18 +116,18 @@ if (hasCameraPermission()) { ### Key points about the code - If the user has already granted your app the required permissions, the `initializeGlassesFeatures` function is called to initialize your app's experience. This function is defined as [part of your app's activity for AI - glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity#create-activity). + glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/first-activity#create-activity). ## Understand the permission request user flow When you launch a permission request using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method, the system initiates a -coordinated user flow across both the AI glasses and the phone. +coordinated user flow across both the glasses and the phone.
> [!IMPORTANT] -> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on AI glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow. +> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on the glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow. > >
> @@ -142,7 +143,7 @@ coordinated user flow across both the AI glasses and the phone. During the permissions user flow, here is what your app and the user can expect: -1. **On the AI glasses** : An activity appears on the **projected device +1. **On the glasses** : An activity appears on the **projected device (glasses)**, instructing the user to look at their phone to continue.
@@ -168,10 +169,10 @@ During the permissions user flow, here is what your app and the user can expect: 3. **On the phone** : If the user accepts the rationale, a modified Android system permission dialog appears on the phone telling the user that they are - granting the permission **for the AI glasses device** (not the phone), and - the user can formally grant or deny the permission. + granting the permission **for the glasses** (not the phone), and the user + can formally grant or deny the permission. 4. **Receiving the result** : After the user makes their final choice, the - activities on both the phone and AI glasses are dismissed. Your + activities on both the phone and glasses are dismissed. Your [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map containing the granted status for each requested permission. \ No newline at end of file From a8f3525d5a1f7fe250fecd0fca43a87755433c8f Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 21 May 2026 21:17:59 +0000 Subject: [PATCH 24/61] Updates skills (2026-05-21 21:17) --- devtools/android-cli/SKILL.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md index 595401d..03dc2cc 100644 --- a/devtools/android-cli/SKILL.md +++ b/devtools/android-cli/SKILL.md @@ -17,6 +17,14 @@ metadata: This skill provides instructions for using the `android` CLI tool. The tool includes various commands for creating projects, running applications, interacting with devices, and managing the CLI environment. +## Installation + +If the android tool is not in the path, install it. To install run the following command: +Linux: curl -fsSL https://dl.google.com/android/cli/latest/linux_x86_64/install.sh | bash +Mac Arm: curl -fsSL https://dl.google.com/android/cli/latest/darwin_arm64/install.sh | bash +Mac Intel: curl -fsSL https://dl.google.com/android/cli/latest/darwin_x86_64/install.sh | bash +Windows: curl -fsSL https://dl.google.com/android/cli/latest/windows_x86_64/install.cmd -o "%TEMP%\i.cmd" && "%TEMP%\i.cmd" + ## SDK management To manage the installation of Android SDKs and tools, use the `sdk` command. For example: @@ -89,6 +97,7 @@ Commands: screen Commands to view the device sdk Download and list SDK packages skills Manage skills + studio Android Studio commands update Update the Android CLI create @@ -201,7 +210,22 @@ skills list List available skills find Find skills by keyword +studio + Usage: android studio [-h] [COMMAND] + Android Studio commands + -h, --help Show this help message and exit. + Commands: + find-declaration Find declaration of a symbol + find-usages Find usages of a symbol + open-file Open a file in Android Studio + check Check the status of running Studio instances + analyze-file Analyze a file in Android Studio + render-compose-preview Render a Compose preview in Android Studio + version-lookup Looks up the latest available versions on the + internet of maven artifacts, Android versions, and + more. + update Usage: android update [--url=PARAM] Update the Android CLI - --url=PARAM The URL to download the update from \ No newline at end of file + --url=PARAM The URL to download the update from From d41bdf526a203142fce6d70804d3e69c8d79f400 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 4 Jun 2026 10:51:54 +0000 Subject: [PATCH 25/61] Updates skills (2026-06-04 10:51) --- build/agp/agp-9-upgrade/SKILL.md | 2 +- identity/verified-email/SKILL.md | 2 +- .../email-verification-implementation.md | 4 ++ jetpack-compose/theming/styles/SKILL.md | 2 +- .../develop/ui/compose/styles/fundamentals.md | 11 ++-- navigation/navigation-3/SKILL.md | 2 +- play/engage-sdk-integration/SKILL.md | 2 +- .../SKILL.md | 8 +-- .../references/migration-logic.md | 6 +- .../references/version-checklist.md | 14 +++- testing/testing-setup/SKILL.md | 2 +- .../preview/compose-screenshot-testing.md | 10 +-- wear/jetpack-compose-m3/SKILL.md | 64 ++++++++----------- .../SKILL.md | 2 +- .../access-hardware-projected-context.md | 6 +- 15 files changed, 73 insertions(+), 64 deletions(-) diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build/agp/agp-9-upgrade/SKILL.md index e39dd14..c021a22 100644 --- a/build/agp/agp-9-upgrade/SKILL.md +++ b/build/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-04-17' + last-updated: '2026-06-03' keywords: - Android Gradle Plugin 9 - AGP 9 diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 6e4a9fa..872570e 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-05-16' + last-updated: '2026-05-22' keywords: - implementation - Android 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 e1d5b32..e34dc18 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,3 +1,7 @@ +> [!IMPORTANT] +> **Important:** We released an agent skill to help you implement email verification with the Digital Credentials API. Try out the skill from the [Android skills +> repository](https://github.com/android/skills). + 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. diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index 70d3d44..ac12ecb 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-05-19' + last-updated: '2026-05-30' keywords: - Jetpack Compose - Styles 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 5361bf5..4a61620 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 @@ -14,8 +14,9 @@ of properties. | Grouping | Properties | Inherited by children | |---|---|---| | **Layout and sizing** | | | -| Padding | `contentPadding` (inner) and `externalPadding` (outer). Available in directional, horizontal, vertical, and all-side variants. | No | -| Dimensions | `fillWidth/Height/Size()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No | +| Content Padding (inner) | - `contentPadding(all: Dp)` - `contentPadding(horizontal: Dp, vertical: Dp)` - `contentPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `contentPaddingHorizontal(value: Dp)` / `contentPaddingVertical(value: Dp)` - `contentPaddingStart(value: Dp)` / `contentPaddingTop(value: Dp)` / `contentPaddingEnd(value: Dp)` / `contentPaddingBottom(value: Dp)` | No | +| External Padding (outer) | - `externalPadding(all: Dp)` - `externalPadding(horizontal: Dp, vertical: Dp)` - `externalPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `externalPaddingHorizontal(value: Dp)` / `externalPaddingVertical(value: Dp)` - `externalPaddingStart(value: Dp)` / `externalPaddingTop(value: Dp)` / `externalPaddingEnd(value: Dp)` / `externalPaddingBottom(value: Dp)` | No | +| Dimensions | `fillWidth()/fillHeight()/fillSize()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No | | Positioning | `left/top/right/bottom` offsets. | No | | **Visual Appearance** | | | | Fills | `background` and `foreground` (supports `Color` or `Brush`). | No | @@ -23,7 +24,7 @@ of properties. | Shape | `shape` | No - but used in conjunction with other properties. `clip` and `border` use this defined shape. | | Shadows | `dropShadow`, `innerShadow` | No | | **Transformations** | | | -| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/Y`, `rotationX/Y/Z` | No | +| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/scaleY`, `rotationX/rotationY/rotationZ` | No | | Control | `alpha`, `zIndex` (stacking order), and `transformOrigin` (pivot point) | No | | **Typography** | | | | Styling | `textStyle`, `fontSize`, `fontWeight`, `fontStyle`, and `fontFamily` | Yes | @@ -82,8 +83,8 @@ Row(
-Similar to the `style` parameter, you can include properties like `background` -or `padding` inside the lambda. +Similar to the `style` parameter, you can include properties like `background`, +`contentPadding`, or `externalPadding` inside the lambda. ```kotlin diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index ef1f7ff..e32b602 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-05-15' + last-updated: '2026-06-02' keywords: - recipe - Android diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 7c4548d..349d48c 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-05-15' + last-updated: '2026-06-02' keywords: - android - engage diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md index 0a63a95..4db8953 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-05-20' + last-updated: '2026-06-02' keywords: - android - play billing @@ -33,7 +33,7 @@ upgrade Play Billing Library to the latest version." 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. +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**. @@ -59,7 +59,7 @@ accomplished afterwards.* ### Step 1: SDK \& Environment Alignment -- **Action** : Update `build.gradle` to meet SDK requirements (e.g., "PBL 8 requires `compileSdk` 35"). +- **Action** : Update `build.gradle` to meet SDK requirements (e.g., "PBL 9 requires `compileSdk` 35"). - **Gradle Version**: Verify if the new library requires a newer Android Gradle Plugin (AGP) or Kotlin version. ### Step 2: Intent-based Refactoring @@ -78,7 +78,7 @@ string replacement. ### Step 4: Final Validation Checklist -1. **Smart Checklist Verification:** +1. **Smart Checklist Verification**: 2. Open [references/version-checklist.md](references/version-checklist.md) and locate the **Smart Version-Specific Checklist**. 3. **Action**: For every version between your \[Detected Effective Version\] and \[Detected New Version\], verify that every item has been addressed in the code using "Find in Files" or structural analysis. 4. **Tests** : Run all unit and implementation tests (`./gradlew test`). 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 ac4d9c5..227a537 100644 --- a/play/play-billing-library-version-upgrade/references/migration-logic.md +++ b/play/play-billing-library-version-upgrade/references/migration-logic.md @@ -63,7 +63,8 @@ checks. ### 6. SDK \& Environment Requirements - **PBL 7.0** : Requires `compileSdk 34` or higher. -- **PBL 8.0** : Requires `compileSdk 35`. +- **PBL 8.0** : Requires `compileSdk 34` or higher. +- **PBL 9.0** : Requires `compileSdk 35` or higher. - **Kotlin** : Verify that `kotlin-stdlib` is updated to at least 1.9.x to support new library coroutine extensions. ### 7. User-Facing Features (Post-Upgrade Recommendations) @@ -73,4 +74,5 @@ versions: - **v7** : **Installments** (Monthly payments for annual plans). - **v8** : **Prepaid Plans** (Users can top-up time without auto-renewing). -- **v8** : **Personalized Pricing** (Show legal disclosure if price varies by user). \ No newline at end of file +- **v8** : **Personalized Pricing** (Show legal disclosure if price varies by user). +- **v9** : **In-App Price Reviews** (Users can accept upcoming subscription price increases directly inside the app without leaving for the Play Store; capped by Google at a maximum of once every 7 days). \ No newline at end of file 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 24ffd41..98f5831 100644 --- a/play/play-billing-library-version-upgrade/references/version-checklist.md +++ b/play/play-billing-library-version-upgrade/references/version-checklist.md @@ -34,13 +34,21 @@ Use this checklist to verify that every technical requirement between your ## PBL v8.x Series -- \[ \] **SDK Compliance** : `compileSdk` is set to 35. +- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher. - \[ \] **Terminology Shift**: Rename "in-app items" to "one-time products" in UI/strings. - \[ \] **Signature Enforcement** : `onProductDetailsResponse` signature MUST be `(BillingResult, QueryProductDetailsResult)`. +- \[ \] **SUBS Offer Token** : For `SUBS` type, `ProductDetailsParams` MUST include an `offerToken`. - \[ \] **Auto-Reconnection** : Verify `enableAutoServiceReconnection()` is used in the builder. - \[ \] **Min SDK Increase** : Verify `minSdkVersion` is at least 23. -## Future Versions (PBL 9.0.0+) +## PBL v9.x Series -- \[ \] **Dynamic Checklist Generation** : For any version \>=9.0.0, you **MUST** synthesize a new checklist for each new version header found in the [Release Notes](https://developer.android.com/google/play/billing/release-notes). +- \[ \] **SDK Compliance** : `compileSdk` is set to 35 or higher. +- \[ \] **API Removals** : Ensure all usages of `SkuDetails`, `SkuDetailsParams`, and `SkuDetailsResponseListener` are removed and replaced with their `ProductDetails` equivalents. +- \[ \] **Blocked Store Error Handling** : If targeting restricted device modes (e.g., kids modes), ensure `androidx.core` is at v1.9+ and update error logic to catch `BILLING_UNAVAILABLE` instead of `ERROR`. +- \[ \] **External Payment Program Null-Safety** : If using developer-provided billing, update `DeveloperProvidedBillingDetails.getLinkUri()` logic to explicitly check for both `null` and empty string (`""`) values before parsing. + +## Future Versions (PBL 10.0.0+) + +- \[ \] **Dynamic Checklist Generation** : For any version \>=10.0.0, you **MUST** synthesize a new checklist for each new version header found in the [Release Notes](https://developer.android.com/google/play/billing/release-notes). - \[ \] **Identify Version Delta** : Review "Breaking Changes" and "Removed APIs" for the new version and create a list of terms to `grep`. \ No newline at end of file diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md index 6223de3..d92e36f 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-05-15' + last-updated: '2026-06-02' keywords: - android - testing 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 ed72c2b..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 @@ -52,7 +52,7 @@ project must meet the following requirements: - Android Studio Panda 1 Canary 4 or higher. - Android Gradle Plugin (AGP) version 9.0 or higher. -- Compose Preview Screenshot Testing plugin version [0.0.1-alpha14](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes#alpha14) 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. - Kotlin version 2.2.10 or higher. - 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). @@ -61,7 +61,7 @@ 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-alpha14](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes#alpha14) 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. - Kotlin version 1.9.20 or higher. We recommend using Kotlin 2.0 or higher so you can use the Compose Compiler Gradle plugin. - 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). @@ -85,15 +85,15 @@ Preview Screenshot Testing plugin. To set up the plugin, follow these steps: experimentalProperties["android.experimental.enableScreenshotTest"] = true } -3. Add the `com.android.compose.screenshot` plugin, version `0.0.1-alpha14` to +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: [versions] agp = "9.0.0-rc03" - kotlin = "2.1.20" - screenshot = "0.0.1-alpha14" + kotlin = "2.2.10" + screenshot = "0.0.1-alpha15" [plugins] screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} diff --git a/wear/jetpack-compose-m3/SKILL.md b/wear/jetpack-compose-m3/SKILL.md index 6dd5ade..cd15d13 100644 --- a/wear/jetpack-compose-m3/SKILL.md +++ b/wear/jetpack-compose-m3/SKILL.md @@ -8,7 +8,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-20' + last-updated: '2026-05-23' keywords: - Wear OS - Compose @@ -19,31 +19,25 @@ metadata: - ScreenScaffold --- -## Prerequisites \& Compatibility +## Prerequisites and compatibility 1. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. - - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`) - 1. **Strict Compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. -2. **Kotlin Version:** For Wear Compose Material3, use Kotlin **2.0.0 or + - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`). +2. **Strict compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. +3. **Kotlin version:** For Wear Compose Material3, use Kotlin **2.0.0 or higher**. -3. **Compose Compiler:** +4. **Compose compiler:** - If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin. - - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility - Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). - 1. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). -4. **Sample Extraction Mandate**: Wear Compose libraries ship with an - additional JAR file which contains individual samples for each and every - component. You MUST NOT propose code changes until the samples in Capability - - files are incomplete and NOT a substitute for these samples; bypassing - extraction is an environment setup failure. + - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). +5. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). +6. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You MUST NOT propose code changes until the samples in Capability 2 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure. ## Gotchas -1. **Mandatory Sync \& Validation:** After updating versions in `libs.versions.toml` or `build.gradle.kts`, you **must** perform a Gradle sync before refactoring any code. This ensures the environment has resolved the libraries correctly. -2. **Prohibition of Guessing (Error Protocol):** If you encounter an 'Unresolved Reference' or API mismatch after a successful sync, do not attempt to 'fix' it by downgrading the library version. +1. **Mandatory sync and validation:** After updating versions in `libs.versions.toml` or `build.gradle.kts`, you **must** perform a Gradle sync before refactoring any code. This ensures the environment has resolved the libraries correctly. +2. **Prohibition of guessing (error protocol):** If you encounter an 'Unresolved Reference' or API mismatch after a successful sync, do not attempt to 'fix' it by downgrading the library version. -## Capabilities and Tools +## Capabilities and tools ### Capability 1: Migration @@ -51,11 +45,11 @@ Use this guidance when migrating from an older version of Wear OS Compose or Horologist. 1. Unless otherwise indicated by the developer, use the latest stable version of Wear Compose Material3 from `{VERSION}`. -2. Read the [migration guide](references/android/training/wearables/compose/migrate-to-material3.md) +2. Read the [migration guide](references/android/training/wearables/compose/migrate-to-material3.md). 3. Use the official component mappings from the migration guide. -4. Before refactoring any component (e.g., `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. +4. Before refactoring any component (for example, `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. 5. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. -6. **Always** check against the component guidance in Capability #3. +6. **Always** check against the component guidance in Capability 3. 7. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. ### Capability 2: Component samples @@ -79,17 +73,17 @@ before proceeding. 1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. 2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. -#### Step 2: Check the Local Cache +#### Step 2: Check the local cache 1. Define the cache directory path: `/samples/{VERSION}/`. Do NOT choose your own different location. 2. Check if this directory exists and contains subdirectories with `.kt` files. - - **IF YES (Cache Hit):** Proceed to **Step 4**. - - **IF NO (Cache Miss):** Proceed to **Step 3**. + - **IF YES (cache hit):** Proceed to **Step 4**. + - **IF NO (cache miss):** Proceed to **Step 3**. -#### Step 3: Check the Gradle Cache +#### Step 3: Check the Gradle cache 1. Sample sources are stored in the Gradle cache. To avoid slow, brute-force searches: - - Determine the Gradle User Home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). + - Determine the Gradle user home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). - The cache root is `/caches`. Call this ``. 2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include "navigation3" in the list if the `androidx.wear.compose.navigation3` library is being used. 3. For each `{ARTIFACT}` in the list: @@ -109,14 +103,14 @@ before proceeding. 6. Proceed **directly to step 4**. -#### Step 4: Read Samples and Implement +#### Step 4: Read samples and implement 1. Read the relevant `.kt` sample files. 2. Use these official, version-matched samples as the for: - Required parameters and slot names. - Default styling and typography tokens. - - Interactive behaviors (e.g., `onClick`, `onLongClick`). - - Component nesting (e.g., `AppScaffold` -\> `ScreenScaffold`). + - Interactive behaviors (for example: `onClick`, `onLongClick`). + - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). ### Capability 3: Component guidance @@ -128,9 +122,7 @@ the component syntax. - \[ \] Use `AppScaffold` as the outer container, with `ScreenScaffold` children. - \[ \] Use only **ONE** `AppScaffold` and any number of `ScreenScaffold`. 2. `ScalingLazyColumn` - Use `TransformingLazyColumn` instead. -3. `TransformingLazyColumn` - - You will need the following imports: +3. `TransformingLazyColumn` - You will need the following imports: ```kotlin @@ -197,7 +189,7 @@ the component syntax.
- - \[ \] Use `TransformingLazyColumn` instead of `ScalingLazyColumn` + - \[ \] Use `TransformingLazyColumn` instead of `ScalingLazyColumn`. - \[ \] You must pass the `contentPadding` parameter from `ScreenScaffold` to the `TransformingLazyColumn`. - \[ \] Use the `minimumVerticalContentPadding` modifier to achieve required padding top and bottom. - This expects a value from defaults, such as `ButtonDefaults`, `CardDefaults`, \`ListHeaderDefaults. @@ -226,7 +218,7 @@ the component syntax. 4. `ScreenScaffold` - - \[ \] Guard the `scrollIndicator` with `!LocalScrollCaptureInProgress.current` + - \[ \] Guard the `scrollIndicator` with `!LocalScrollCaptureInProgress.current`. 5. `EdgeButton` - \[ \] Do **NOT** use as the final item within a `TransformingLazyColumn`. Instead, use the slot in `ScreenScaffold`. @@ -272,11 +264,11 @@ the component syntax. - \[ \] Do **NOT** hard-code text sizes, use `typography` from `MaterialTheme`. - \[ \] Do **NOT** hard-code colors, use `colorScheme` from `MaterialTheme`. -8. Use component defaults +8. Use component defaults: - \[ \] Components such as `Button` have a corresponding `ButtonDefaults` object. - Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. -9. Use Wear specific previews +9. Use Wear specific previews: - \[ \] `WearPreviewDevices` - \[ \] `WearPreviewFontScales` diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 321f178..52d6c81 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-20' + last-updated: '2026-06-03' keywords: - Jetpack Compose Glimmer - audio glasses diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md index ee543de..cecef35 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md @@ -141,8 +141,10 @@ audioRecord.startRecording() define a sample rate of 16kHz and a channel configuration of either mono or stereo (using [`CHANNEL_IN_MONO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_mono) or [`CHANNEL_IN_STEREO`](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel_in_stereo)). -- While there is no fixed requirement for buffer size, [get the minimum buffer - size](https://developer.android.com/reference/kotlin/android/media/AudioRecord#getminbuffersize) to minimize perceived latency. +- Use [`AudioRecord.getMinBufferSize()`](https://developer.android.com/reference/kotlin/android/media/AudioRecord#getminbuffersize) to determine the minimum buffer + size to create the `AudioRecord` object. However, to prevent audio drops + from the glasses, you should read from this buffer in short, frequent chunks + (ideally 20ms slices) rather than waiting for the entire buffer to fill. > [!WARNING] > **Preview:** Support for using the [`MediaRecorder`](https://developer.android.com/reference/kotlin/android/media/MediaRecorder) API with a projected context will be available in an upcoming Android Beta release. From 8e9889a6e9da04733e83185421e79bdf3a91e354 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 11 Jun 2026 08:35:19 +0000 Subject: [PATCH 26/61] Updates skills (2026-06-11 08:35) --- devtools/android-cli/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md index 03dc2cc..1938645 100644 --- a/devtools/android-cli/SKILL.md +++ b/devtools/android-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: android-cli -description: Orchestrates Android development tasks including project creation, deployment, SDK management, and environment diagnostics using the `android` command-line tool. +description: Provides instructions for installing and using the `android` CLI. The `android` command-line tool is a critical tool for Android development and helps you create new Android projects, run Android apps on devices, manage and interact with Android virtual devices (including screenshots and UI inspection), manage Android SDK components, look up official Android documentation, and discover and install official Android skills. license: Complete terms in LICENSE.txt metadata: author: Google LLC From fe95b6fdf002ece6c7340013623d41d8deec6f52 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Thu, 11 Jun 2026 08:38:23 +0000 Subject: [PATCH 27/61] Updates skills (2026-06-11 08:38) --- identity/verified-email/SKILL.md | 4 +- .../digital-credentials/email-verification.md | 2 +- .../identity/passkeys/create-passkeys.md | 34 +-- .../SKILL.md | 2 +- jetpack-compose/theming/styles/SKILL.md | 2 +- .../ui/compose/styles/state-animations.md | 16 +- performance/r8-analyzer/SKILL.md | 2 +- .../enable-app-optimization.md | 39 ++-- play/engage-sdk-integration/SKILL.md | 2 +- .../android/guide/playcore/engage/faq.md | 217 +++++++++--------- .../engage/tv/continue-watching/index.md | 17 +- .../guide/playcore/engage/tv/entitlements.md | 12 +- .../playcore/engage/tv/getting-started.md | 7 +- .../playcore/engage/tv/recommendations.md | 6 +- wear/jetpack-compose-m3/SKILL.md | 4 +- .../wearables/compose/migrate-to-material3.md | 10 +- .../SKILL.md | 2 +- 17 files changed, 197 insertions(+), 181 deletions(-) diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 872570e..31d185f 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-05-22' + last-updated: '2026-06-09' keywords: - implementation - Android @@ -96,7 +96,7 @@ Find the business logic handling user attributes and account creation, account recovery: - **Keywords** : `SignUpViewModel`, `AuthViewModel`, `RegistrationRepository`. -- **Code Pattern** : Look for methods like `onCrea teAccount`, `onRecoverAccount`, or `validateEmail`. +- **Code Pattern** : Look for methods like `onCreateAccount`, `onRecoverAccount`, or `validateEmail`. ### 3. Find instances of reauthentication for sensitive actions 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 ac696ee..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 @@ -106,7 +106,7 @@ 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. -> For phone number verification, see [Firebase Phone Number Verification](https://firebase.google.com/docs/phone-number-verification). Note that you can't request a phone number and email in the same invocation as these are separate capabilities. +> For phone number verification, see [Verify phone numbers with digital credentials](https://developer.android.com/identity/digital-credentials/phone-number-verification). For a Firebase solution, see [Firebase Phone Number Verification](https://firebase.google.com/docs/phone-number-verification). Note that you can't verify a phone number and email in the same invocation as these are separate capabilities. ## Validity and freshness 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 1ae9341..bfe19e5 100644 --- a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -133,17 +133,19 @@ Key fields in the public key creation options include: - `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. +- `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. +- `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 @@ -171,12 +173,12 @@ 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). +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] 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 1597339..a6005e3 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-05-19' + last-updated: '2026-06-08' keywords: - Jetpack Compose - migration diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index ac12ecb..04d8e21 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-05-30' + last-updated: '2026-06-06' keywords: - Jetpack Compose - Styles 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 bffc185..ecfbaeb 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 @@ -318,11 +318,11 @@ var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } -fun StyleScope.playerPlaying(value: Style) { - state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) +fun StyleScope.playerPlaying(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) } -fun StyleScope.playerPaused(value: Style) { - state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) +fun StyleScope.playerPaused(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) } ``` @@ -403,11 +403,11 @@ var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } -fun StyleScope.playerPlaying(value: Style) { - state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) +fun StyleScope.playerPlaying(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing }) } -fun StyleScope.playerPaused(value: Style) { - state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) +fun StyleScope.playerPaused(block: () -> Unit) { + state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) } diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 3d7ce47..7ad9e5e 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-05-19' + last-updated: '2026-06-09' keywords: - R8 - proguard 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 9728527..69c4295 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 @@ -104,6 +104,28 @@ android { } ``` +## 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: + +- 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) +- Enable resource shrinking and [optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) +- [Refine keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-best-practices) to allow maximum optimization of classes, fields and methods. + +To help you refine keep rules, use the [R8 Configuration Analyzer](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). + +The R8 Configuration Analyzer lets you do the following: + +- Track and improve the overall R8 configuration quality by monitoring the metrics provided by the R8 Configuration Analyzer report. +- 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). + ## Optimize resource shrinking for even smaller apps The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource @@ -161,23 +183,6 @@ fix them: 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. -## 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, enable -R8 in full mode and refine the keep rules to allow maximum optimization of -classes, fields and methods.To help you with this, use the -[R8 Configuration Analyzer](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). - -The R8 Configuration Analyzer lets you do the following: - -- Track and improve the overall R8 configuration quality by monitoring the metrics provided by the R8 Configuration Analyzer report. -- 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). - ## AGP and R8 version behavior changes The following table outlines the key features introduced in various versions of diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 349d48c..24b4fd1 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-06-02' + last-updated: '2026-06-09' keywords: - android - engage 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 976ad16..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 @@ -103,58 +103,63 @@ calculate impact. ### General Questions -#### What is Continue Watching 2.0? +#### What is Engage? -Continue Watching 2.0 (Video Discovery API) 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 Continue Watching 2.0. +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. -#### What are the benefits of using Continue Watching 2.0? +#### Is Video Discovery API the same as Engage? -Answer: Continue Watching 2.0 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: +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: - 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: Continue Watching 2.0 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, Continue Watching 2.0 works across other Android media experiences, like Play Cubes 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! Continue Watching 2.0 is backward compatible, so your existing integration will still work. +- 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 Continue -Watching 2.0. 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 Continue Watching 2.0? +#### What surfaces support Engage? 1. Google TV 2. Android TV (on-device only but supports Engage SDK) 3. Google TV Android mobile app 4. Google TV iOS mobile app -5. Play Cubes +5. Play Collections 6. Google entertainment space 7. iOS devices (with REST API integration). -#### Is Engage SDK for Continue Watching 2.0? +#### Is Engage SDK for Continue Watching? -Yes, the Engage SDK is for Continue Watching 2.0. It is required to integrate -with Continue Watching 2.0. +Yes, the Engage SDK supports content for the continue watching row. It is +required to integrate with Engage. -#### Is Continue Watching 2.0 available for everyone? +#### Is Engage available for everyone? -Continue Watching 2.0 is being rolled out in phases. +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 Continue Watching 2.0 available to all developers soon. +- 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 Continue Watching 2.0 -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? @@ -163,24 +168,24 @@ 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 advantages for Continue Watching, 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. -- **Mobile app integration:** Continue Watching is available on the Google TV +- **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, Continue Watching prompts users to re-engage - with your app, boosting retention. + 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:** This integration extends Continue Watching - to other Google media platforms like Android, Play Cubes, 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? @@ -191,7 +196,7 @@ providers. #### What happens if I try to publish more than 5 entities? -EngageSDK will reject your publish request if it exceeds the 5-entity limit. +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 @@ -199,10 +204,10 @@ 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 @@ -211,10 +216,10 @@ promoting a fair and balanced user experience. 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 Continue -Watching 2.0 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. @@ -227,9 +232,8 @@ 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 Continue -Watching 2.0 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): @@ -260,43 +264,44 @@ experience for your users. ### Account and Profile Questions -#### My app uses anonymous user logins. Is AccountProfile still required for Continue Watching 2.0? +#### 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 Continue Watching 2.0 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 Continue Watching 2.0 even if your app doesn't have a user account system. -- Limited to on-device use: The cross-device capabilities of Continue Watching 2.0 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 Continue Watching entries only appear on the specific device where the content was started. +- `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 Continue Watching 2.0 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? +#### 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. +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 Continue Watching 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 Continue Watching but only use accountId when deleting content, the system may not be able to correctly identify and remove the intended items. +- 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. -#### Is profileId required for Continue Watching 2.0? +#### Is `profileId` required for Engage? -- accountId is required. This identifies the user across devices. -- profileId is crucial for a good user experience. While technically optional, profileId is strongly recommended if your service supports multiple profiles (like many streaming services do). Why is it so important? Because without profileId, Continue Watching may show content from other profiles on the same account. This can lead to a confusing and frustrating experience for your users. -- In short: Providing profileId makes sure that Continue Watching accurately reflects each individual's viewing history. Unless your app doesn't support the concept of profile within an account, you should provide it. +- `accountId` is required. This identifies the user across devices. +- `profileId` is crucial for a good user experience. While technically optional, `profileId` is strongly recommended if your service supports multiple profiles (like many streaming services do). Why is it so important? Because without `profileId`, the continue watching row may show content from other profiles on the same account. This can lead to a confusing and frustrating experience for your users. +- In short: Providing `profileId` makes sure that content provided to the continue watching row accurately reflects each individual's viewing history. Unless your app doesn't support the concept of profile within an account, you should provide it. -#### How does Google use the profileId on their side? +#### 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 +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 accountId-profileId combination. Any Google device that has -that same Google Account logged in, would get the latest updated data from the -same associated accountId-profileId(s), in its ContinueWatching row. +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 Continue Watching 2.0? +#### 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. @@ -312,10 +317,10 @@ to the current device. #### Can we set "sync across devices" to false? -UserConsentToSyncAcrossDevices flag controls whether a user's +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 only happens -on the same device. +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. @@ -336,16 +341,15 @@ profile ID (that was deposited on android). #### Is there documentation on the REST API? -The ETA for REST API is March 2025, this is documented in Continue Watching 2.0 -Developer Docs. +The ETA for REST API is March 2025, this is documented in Engage Developer Docs. ### Legacy Watch Next Questions -#### Is the Video Discovery API replacing the Watch Next API? +#### Is Engage replacing the Watch Next API? -The Video Discovery API will be backward compatible on all Android TV devices -that support the Watch Next API. All developers should use Video Discovery API -(Continue Watching 2.0) to publish to the Continue Watching row. +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 @@ -363,32 +367,32 @@ the content. 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 -3. Changing accounts on the GTV device should change the cards on the Continue Watching row. Only videos from the current account should show up. Sorted in recent order. 3p app profile CW will be intermixed. Note: CW for GoogleAccount2 will show 3P contents that GoogleAccount2 was engaged watching +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 +3. Changing accounts on the GTV device should change the cards on the continue watching row. Only videos from the current account should show up. Sorted in recent order. 3p app profile CW will be intermixed. Note: CW for GoogleAccount2 will show 3P contents that GoogleAccount2 was engaged watching 4. Exit the app with BACK button \> Verify card is displayed in the "Continue watching" row -5. Hide the video in the "Continue Watching" row, it shouldn't show again Test if hidden content stays hidden beyond 24 hours and even after the app opens after 24 hrs. Confirm hiding one item does not hide multiple items. -6. Content availability in Continue watching with full metadata: Card image, App name, title, season episode # for TV contents +5. Hide the video in the continue watching row, it shouldn't show again Test if hidden content stays hidden beyond 24 hours and even after the app opens after 24 hrs. Confirm hiding one item does not hide multiple items. +6. Content availability in the continue watching row with full metadata: Card image, App name, title, season episode # for TV contents 7. Check Progress displays in the progress bar -8. User watched the content until ending credits - content does not display in Continue Watching -9. Confirm no unwatched items show up in continue watching row +8. User watched the content until ending credits - content does not display in the continue watching row +9. Confirm no unwatched items show up in the continue watching row 10. Confirm that the CW items are arranged chronologically based on when watch activity happened and not when the app was last opened or last day 11. Confirm that episode and season details on CW card match what was watched on episodic content -12. Confirm completed (items at credits or beyond) items don't show up in continue watching +12. Confirm completed (items at credits or beyond) items don't show up in the continue watching row 13. Turn off the device halfway through watching the episode/movie/show. "Turn off the device halfway through watching the episode/movie/show. Verify on turning on the device and on other TV, CW displays the right card , at the right position and progress bar" 14. Turn off the device after completely watching episode 1, verify -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\] +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 + 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 + 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. @@ -396,21 +400,21 @@ cases can be performed on other surfaces as well. > [!NOTE] > **Note:** When the app is installed and logged in, the CW state would reflect the active 3P user logged into the 3P app. - 1. Note: Continue Watching row shouldn't show 3P contents if the GoogleAccount is not yet associated with a 3P profile + 1. Note: the continue watching row shouldn't show 3P contents if the GoogleAccount is not yet associated with a 3P profile -#### We are not seeing Continue Watching showing up on Google TV iOS app. What happened? +#### We are not seeing continuation content showing up on Google TV iOS app. What happened? -You will need to send iOS deeplinks for Continue Watching to show up 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 Continue Watching information? Should I update the Continue Watching information frequently, like every 15 seconds? +#### How often should I update content information for the continue watching row? Should I update it frequently, like every 15 seconds? No, frequent updates are not recommended. Here's why: - Performance Impact: Continuously sending updates puts unnecessary strain on our servers, potentially slowing down the system for everyone. - Unnecessary Data: While a user is actively watching, their playback position changes constantly. Sending updates every few seconds creates a lot of redundant data that isn't helpful for resuming playback. -When to update Continue Watching information: +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: @@ -424,11 +428,12 @@ How to efficiently update: 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. +- `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 Continue Watching experience for your users. +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/tv/continue-watching/index.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md index affe437..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,18 +1,19 @@ -Continue Watching leverages the **Continuation cluster** to show unfinished +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 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: -- [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) +- [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 the video -discovery APIs to send personalized user data to Google, build a common module -that you can import in both the mobile and TV apps, when to call the publish -and delete APIs, and 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 0984715..a2ce68c 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 @@ -7,7 +7,7 @@ mobile, and tablet. ## Prerequisites > [!IMPORTANT] -> **Important:** [Express interest in integrating with the Video Discovery API](http://g.co/tv/vda). +> **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 @@ -248,16 +248,16 @@ maintained. > [!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 Video discovery data, manually delete a user's data from the +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 video discovery + `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: // If the user logs out from your media app, you must make the following call - // to remove subscription and other video discovery data from the current + // to remove subscription and other Engage data from the current // google TV device. client.deleteClusters( new DeleteClustersRequest.Builder() @@ -270,7 +270,7 @@ maintained. when user revokes the consent: // If the user revokes the consent to share across device, make the call - // to remove subscription and other video discovery data from all google + // to remove subscription and other Engage data from all google // TV devices. client.deleteClusters( new DeleteClustersRequest.Builder() @@ -283,7 +283,7 @@ maintained. deletion. // If the user delete a specific profile, you must make the following call - // to remove subscription data and other video discovery data. + // to remove subscription data and other Engage data. client.deleteClusters( new DeleteClustersRequest.Builder() .setAccountProfile(accountProfile) 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 0d7ffdc..83c50aa 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 @@ -4,7 +4,7 @@ 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 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) - [Publish recommendations](https://developer.android.com/guide/playcore/engage/tv/recommendations) @@ -12,7 +12,7 @@ of the TV features: Before you begin, complete the following steps: -1. [Express interest in developing the Video Discovery API](http://g.co/tv/vda) to enroll in +1. [Express interest in developing with Engage](http://g.co/tv/vda) to enroll in the program, if eligible. 2. Verify that your app targets Android 4.4 (API level 19) or higher for this @@ -83,6 +83,9 @@ optionally a profile ID and locale. .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). + ## Testing To test your integration, download the verification app: 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 478747d..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 @@ -10,7 +10,7 @@ can be a maximum **of 7** recommendation clusters per request. ## Pre-work > [!IMPORTANT] -> **Important:** [Express interest in developing the Video Discovery API](http://g.co/tv/vda). +> **Important:** [Express interest in developing with Engage](http://g.co/tv/vda). Complete the [Pre-work](https://developer.android.com/guide/playcore/engage/tv/getting-started#pre-work) instructions in the Getting Started guide. @@ -96,11 +96,11 @@ 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 video discovery data +### 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 video discovery +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. diff --git a/wear/jetpack-compose-m3/SKILL.md b/wear/jetpack-compose-m3/SKILL.md index cd15d13..84b8be2 100644 --- a/wear/jetpack-compose-m3/SKILL.md +++ b/wear/jetpack-compose-m3/SKILL.md @@ -8,7 +8,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-23' + last-updated: '2026-06-06' keywords: - Wear OS - Compose @@ -106,7 +106,7 @@ before proceeding. #### Step 4: Read samples and implement 1. Read the relevant `.kt` sample files. -2. Use these official, version-matched samples as the for: +2. Use these official, version-matched samples as the source of truth for: - Required parameters and slot names. - Default styling and typography tokens. - Interactive behaviors (for example: `onClick`, `onLongClick`). diff --git a/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md index 3123a04..1969a7d 100644 --- a/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ b/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5: ### M3 - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha03") + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha04") See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). -Wear Compose Foundation library version 1.7.0-alpha03 introduced +Wear Compose Foundation library version 1.7.0-alpha04 introduced some new components that are designed to work with Material 3 components. Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an updated animation when running on Wear OS 6 (API level 36) or higher. When updating to Wear Compose Material 3 version, we suggest to also update the Wear Compose Foundation and Navigation libraries: - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha03") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha03") + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha04") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha04") ## Theme @@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components: And finally a list of some relevant components from Wear Compose Foundation library: -| Wear Compose Foundation 1.7.0-alpha03 | | +| Wear Compose Foundation 1.7.0-alpha04 | | |---|---| | [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | | [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 52d6c81..b1dab31 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-03' + last-updated: '2026-06-09' keywords: - Jetpack Compose Glimmer - audio glasses From 07302ca15e21d827cab5ca64d46407fb51dbe0aa Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Wed, 24 Jun 2026 14:23:35 +0000 Subject: [PATCH 28/61] Updates skills (2026-06-24 14:23) --- build/agp/agp-9-upgrade/SKILL.md | 2 +- .../build/migrate-to-built-in-kotlin.md | 416 ---------- jetpack-compose/adaptive/SKILL.md | 12 +- .../adaptive/flexbox/container-behavior.md | 2 +- .../layouts/adaptive/flexbox/get-started.md | 2 +- .../adaptive/grid/container-properties.md | 11 +- .../layouts/adaptive/grid/get-started.md | 2 +- .../ui/compose/layouts/adaptive/grid/index.md | 2 +- .../layouts/adaptive/grid/item-properties.md | 4 +- .../SKILL.md | 4 +- .../analysis-of-the-project-and-layout.md | 42 - ...setup-compose-dependencies-and-compiler.md | 58 +- jetpack-compose/theming/styles/SKILL.md | 2 +- .../develop/ui/compose/styles/fundamentals.md | 2 +- .../ui/compose/styles/state-animations.md | 2 +- .../ui/compose/styles/styles-vs-modifiers.md | 4 +- navigation/navigation-3/SKILL.md | 14 +- .../navigation-3/recipes/basicdsl.md | 85 -- .../navigation-3/recipes/deeplinks-basic.md | 744 ------------------ .../navigation-3/recipes/modular-koin.md | 287 ------- play/engage-sdk-integration/SKILL.md | 4 +- .../android/guide/playcore/engage/food.md | 92 ++- .../guide/playcore/engage/healthandfitness.md | 89 ++- .../android/guide/playcore/engage/listen.md | 92 ++- .../guide/playcore/engage/otherverticals.md | 98 ++- .../android/guide/playcore/engage/read.md | 92 ++- .../android/guide/playcore/engage/shopping.md | 97 ++- .../android/guide/playcore/engage/social.md | 86 +- .../android/guide/playcore/engage/travel.md | 94 ++- .../playcore/engage/tv/getting-started.md | 2 +- .../android/guide/playcore/engage/watch.md | 88 ++- .../references/requests.md | 231 ------ .../SKILL.md | 2 +- .../google/play/billing/release-notes.md | 21 + security/android-intent-security/SKILL.md | 554 +++++++++++++ .../SKILL.md | 37 +- .../wearables/compose/migrate-to-material3.md | 0 .../SKILL.md | 6 +- .../references/button-samples-source.md | 176 ----- .../references/liststate-source.md | 420 ---------- 40 files changed, 1479 insertions(+), 2499 deletions(-) delete mode 100644 build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md delete mode 100644 jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md delete mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md delete mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md delete mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md delete mode 100644 play/engage-sdk-integration/references/requests.md create mode 100644 security/android-intent-security/SKILL.md rename wear/{jetpack-compose-m3 => wear-compose-m3}/SKILL.md (90%) rename wear/{jetpack-compose-m3 => wear-compose-m3}/references/android/training/wearables/compose/migrate-to-material3.md (100%) delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build/agp/agp-9-upgrade/SKILL.md index c021a22..1ffa0db 100644 --- a/build/agp/agp-9-upgrade/SKILL.md +++ b/build/agp/agp-9-upgrade/SKILL.md @@ -51,7 +51,7 @@ If Hilt is used in the project, ensure it is on version 2.59.2 or higher. ### Step 2: Migrate to built-in Kotlin -See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information. +See [the guide](https://developer.android.com/build/migrate-to-built-in-kotlin) for detailed information. ### Step 3. Migrate to the new AGP DSL diff --git a/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md b/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md deleted file mode 100644 index 7306e7b..0000000 --- a/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md +++ /dev/null @@ -1,416 +0,0 @@ -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`. - -> [!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. - -## 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: - - Failed to apply plugin 'org.jetbrains.kotlin.android'. - > Cannot add extension with name 'kotlin', as there is an extension already registered with that name. - -...or - - 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: - -> [!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). - -1. [Remove the `kotlin-android` plugin](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-remove-kotlin-android-plugin) -2. [Migrate the `kotlin-kapt` plugin if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-kapt-plugin) -3. [Migrate the `android.kotlinOptions{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-options) -4. [Migrate the `kotlin.sourceSets{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-source-sets) - -### 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. - -### With version catalogs - -### Kotlin - -```kotlin -// Module-level build file -plugins { - alias(libs.plugins.kotlin.android) -} -``` - -### Groovy - -```groovy -// Module-level build file -plugins { - alias(libs.plugins.kotlin.android) -} -``` - -### No version catalogs - -### Kotlin - -```kotlin -// Module-level build file -plugins { - id("org.jetbrains.kotlin.android") -} -``` - -### Groovy - -```groovy -// Module-level build file -plugins { - id 'org.jetbrains.kotlin.android' -} -``` - -Then, remove the plugin from your top-level build file: - -### With version catalogs - -### Kotlin - -```kotlin -// Top-level build file -plugins { - alias(libs.plugins.kotlin.android) apply false -} -``` - -### Groovy - -```groovy -// Top-level build file -plugins { - alias(libs.plugins.kotlin.android) apply false -} -``` - -### No version catalogs - -### Kotlin - -```kotlin -// Top-level build file -plugins { - id("org.jetbrains.kotlin.android") version "KOTLIN_VERSION" apply false -} -``` - -### Groovy - -```groovy -// Top-level build file -plugins { - id 'org.jetbrains.kotlin.android' version 'KOTLIN_VERSION' apply false -} -``` - -If you use version catalogs, also remove the plugin definition from the -version catalog TOML file (usually `gradle/libs.versions.toml`): - -```toml -[plugins] -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "KOTLIN_VERSION" } -``` - -### 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). - -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: - -```toml -[plugins] -android-application = { id = "com.android.application", version.ref = "AGP_VERSION" } - -# Add the following plugin definition -legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "AGP_VERSION" } - -# Remove the following plugin definition -kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "KOTLIN_VERSION" } -``` - -Then, update your build files: - -### Kotlin - -```kotlin -// Top-level build file -plugins { - alias(libs.plugins.legacy.kapt) apply false - alias(libs.plugins.kotlin.kapt) apply false -} -``` - -### Groovy - -```groovy -// Top-level build file -plugins { - alias(libs.plugins.legacy.kapt) apply false - alias(libs.plugins.kotlin.kapt) apply false -} -``` - -### Kotlin - -```kotlin -// Module-level build file -plugins { - alias(libs.plugins.legacy.kapt) - alias(libs.plugins.kotlin.kapt) -} -``` - -### Groovy - -```groovy -// Module-level build file -plugins { - alias(libs.plugins.legacy.kapt) - alias(libs.plugins.kotlin.kapt) -} -``` - -> [!NOTE] -> **Note:** If you declare the `kotlin-kapt` plugin in the `plugins{}` block as `kotlin("kapt") version ""`, then remove that line instead. - -### 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. - -For example, update this code: - -### Kotlin - -```kotlin -android { - kotlinOptions { - languageVersion = "2.0" - jvmTarget = "11" - } -} -``` - -### Groovy - -```groovy -android { - kotlinOptions { - languageVersion = "2.0" - jvmTarget = "11" - } -} -``` - -...to the new DSL: - -### Kotlin - -```kotlin -kotlin { - compilerOptions { - languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0 - // Optional: Set jvmTarget - // jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 - } -} -``` - -### Groovy - -```groovy -kotlin { - compilerOptions { - languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0 - // Optional: Set jvmTarget - // jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 - } -} -``` - -> [!NOTE] -> **Note:** With built-in Kotlin, you don't need to set `kotlin.compilerOptions.jvmTarget` because its value defaults to `android.compileOptions.targetCompatibility`. - -### 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. - -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 - -```kotlin -# Adding Kotlin source directories to kotlin.sourceSets is not supported -kotlin.sourceSets.named("main") { - kotlin.srcDir("additionalSourceDirectory/kotlin") -} - -# Adding Kotlin source directories to AndroidSourceSet.java is also not supported -android.sourceSets.named("main") { - java.directories += "additionalSourceDirectory/kotlin" -} - -# Add Kotlin source directories to AndroidSourceSet.kotlin -android.sourceSets.named("main") { - kotlin.directories += "additionalSourceDirectory/kotlin" -} -``` - -### Groovy - -```groovy -# Adding Kotlin source directories to kotlin.sourceSets is not supported -kotlin.sourceSets.named("main") { - kotlin.srcDir("additionalSourceDirectory/kotlin") -} - -# Adding Kotlin source directories to AndroidSourceSet.java is also not supported -android.sourceSets.named("main") { - java.directories.add("additionalSourceDirectory/kotlin") -} - -# Add Kotlin source directories to AndroidSourceSet.kotlin -android.sourceSets.named("main") { - kotlin.directories.add("additionalSourceDirectory/kotlin") -} -``` - -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 - -```kotlin -androidComponents.onVariants { variant -> - variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin") - variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT) -} -``` - -### Groovy - -```groovy -androidComponents.onVariants { variant -> - variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin") - variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT) -} -``` - -## 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. - -## 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. - -> [!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). - -## 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. - -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. - -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. - -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. - -To disable built-in Kotlin for a module, -set `enableKotlin = false` in that module's build file: - -### Kotlin - -```kotlin -android { - enableKotlin = false -} -``` - -### Groovy - -```groovy -android { - enableKotlin = false -} -``` \ No newline at end of file diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md index 64030c6..26e9f87 100644 --- a/jetpack-compose/adaptive/SKILL.md +++ b/jetpack-compose/adaptive/SKILL.md @@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-05-20' + last-updated: '2026-06-18' keywords: - android - ui @@ -75,11 +75,11 @@ 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 real state for the content, hide the +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 the content is best displayed in full screen (such as a single photo screen). +- 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. @@ -101,7 +101,7 @@ parameter. Steps to migrate: -- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. It could be named something like `isNavBarVisible` or `shouldShowNavBar`. +- Identify the scenarios under which the navigation bar is hidden. This is usually done with a boolean variable for the visibility. Use `isNavBarVisible` or `shouldShowNavBar` as the variable name. - Create an instance of `NavigationSuiteScaffoldState` using `rememberNavigationSuiteScaffoldState()` and pass it to `NavigationSuiteScaffold`. - When the navigation area visibility changes, use a `LaunchedEffect` to call `show` or `hide` on the `NavigationSuiteScaffoldState`. @@ -207,8 +207,8 @@ Look for the following vertical list composables: `LazyColumn`, Steps to migrate: -- Choose a suitable minimum width in dp for the column. It should be large enough so that item is clearly visible to the user. -- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction below +- Choose a suitable minimum width in dp for the column. The item must be clearly visible to the user at this width. +- For `LazyColumn`: change to a `LazyVerticalGrid` and follow the instruction later - For `LazyVerticalGrid`: change the `columns` parameter to use `GridCells.Adaptive(.dp)` - For `LazyVerticalStaggeredGrid`: change the `columns` parameter to use `StaggeredGridCells.Adaptive(.dp)` 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 ad4862f..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 @@ -95,7 +95,7 @@ There is space, so Child 1 is 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. -![First item placed in the FlexBox container.](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. +![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 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 8317e17..f1122fe 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 @@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts. `lib.versions.toml`. [versions] - compose = "1.12.0-alpha03" + compose = "1.12.0-beta01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 63d160c..7625935 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 @@ -112,7 +112,7 @@ 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#intrisic-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#intrinsic-grid-track-size). The following example uses the different track sizing options to define the row heights: @@ -131,6 +131,11 @@ Grid( modifier = Modifier.height(480.dp) ) { PastelRedCard("Fixed(100.dp)") + PastelGreenCard("Percentage(0.2f)") + PastelBlueCard("Flex(1.fr)") + PastelYellowCard("Auto") + +} ```
@@ -163,8 +168,8 @@ Grid( }, modifier = Modifier.width(480.dp) ) { - Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." ) - Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet." ) + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") + Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") } ``` 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 28b8c3d..e47dced 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 @@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co `lib.versions.toml`. [versions] - compose = "1.12.0-alpha03" + compose = "1.12.0-beta01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 1f74aee..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 @@ -25,7 +25,7 @@ of which layouts are suitable for each API: | Component | Purpose | |---|---| -| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that requires lazy loading. | +| `LazyVerticalGrid`, `LazyStaggeredGrid`, `LazyHorizontalGrid` | Visualization of large, homogeneous data sets that require lazy loading. | | `Row`, `Column`, `FlexBox` | One-dimensional layout | | `Grid` | Two-dimensional layout | 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 f5ecc87..9f0500e 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 @@ -137,7 +137,7 @@ 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 specified grid cells, +Card **#2** and Card **#4** are placed in specified grid cells, and the other items are auto-placed. @@ -165,4 +165,4 @@ Grid(
-![Card #3 is placed next to card #1, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to card **#1**, as it is an auto-placement. \ No newline at end of file +![Card #3 is placed next to Card #1, as it is an auto-placement.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/autoplacement-mixed-with-placement.png) **Figure 4** . Card **#3** is placed next to Card **#1**, as it is an auto-placement. \ 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 a6005e3..58a84c7 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-06-08' + last-updated: '2026-06-20' keywords: - Jetpack Compose - migration @@ -56,7 +56,7 @@ following the logic in [references/identify-optimal-xml-candidate.md](references 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 +Use [references/analysis-of-the-project-and-layout.md](https://developer.android.com/agents/skills/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout) to guide your technical audit of the layout and surrounding project context. ### Step 3: Create a plan 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 deleted file mode 100644 index 3e2f0c3..0000000 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md +++ /dev/null @@ -1,42 +0,0 @@ -## 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. - -## 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. - -## 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. - -## 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). - -## 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. - -*** ** * ** *** - -> **Pro-tip:** Always prioritize the "Existing infrastructure" over "Default templates." If the project has a custom way of handling spacing or colors, composable code, or any other project layer, your generated Compose code must reflect that specific implementation. \ 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 5abc55e..bf06fa0 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,5 @@ -## Set up the Compose Compiler Gradle plugin - -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 @@ -14,28 +12,29 @@ Set up the Compose Compiler Gradle plugin: 1. In the `libs.versions.toml` file, remove any reference to the Compose Compiler. 2. In the `versions` and `plugins` sections, add the new dependency: - [versions] - kotlin = "2.3.21" + [versions] + kotlin = "2.3.21" - [plugins] - org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + [plugins] + org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - // Add this line - compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + // Add this line + compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -1. 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 { + plugins { // Existing plugins alias(libs.plugins.compose.compiler) apply false - } + } -1. In each module that uses Compose, apply the plugin: +4. In each module that uses Compose, apply the plugin: - plugins { + plugins { // Existing plugins 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 @@ -75,10 +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.05.00`. +Always use the latest Compose BOM version: `2026.06.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: @@ -104,7 +103,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - def composeBom = platform('androidx.compose:compose-bom:2026.05.00') + def composeBom = platform('androidx.compose:compose-bom:2026.06.00') implementation composeBom androidTestImplementation composeBom @@ -143,7 +142,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - val composeBom = platform("androidx.compose:compose-bom:2026.05.00") + val composeBom = platform("androidx.compose:compose-bom:2026.06.00") implementation(composeBom) androidTestImplementation(composeBom) @@ -180,4 +179,19 @@ 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). \ No newline at end of file +> 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`. + +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 diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index 04d8e21..8a82ccd 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-06-06' + last-updated: '2026-06-11' keywords: - Jetpack Compose - Styles 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 4a61620..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 @@ -297,7 +297,7 @@ Styles](https://developer.android.com/static/develop/ui/compose/styles/images/bu ## Style inheritance > [!NOTE] -> **Note:** Whilst the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`. +> **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 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 ecfbaeb..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 @@ -356,7 +356,7 @@ fun MediaPlayer(
-Within the `style` lambda, you can apply state based styling for custom states, +Within the `style` lambda, you can apply state-based styling for custom states, using the previously defined extension functions. 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 1ffca16..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 @@ -4,7 +4,7 @@ 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 theme-wide set of properties for a component. +- **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. - **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties. The following is a comparison between Styles versus modifiers: @@ -20,7 +20,7 @@ The following is a comparison between Styles versus modifiers: ## Limitations of modifiers Modifiers have many benefits in the current Compose landscape. However, Styles -addresses some limitations of modifiers, which the following list describes: +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. diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index e32b602..87ccd8b 100644 --- a/navigation/navigation-3/SKILL.md +++ b/navigation/navigation-3/SKILL.md @@ -3,12 +3,12 @@ name: navigation-3 description: Learn how to install and migrate to Jetpack Navigation 3, and how to implement features and patterns such as deep links, multiple backstacks, scenes (dialogs, bottom sheets, list-detail, two-pane, supporting pane), conditional navigation - (such as logged-in navigation vs anonymous), returning results from flows, integration + (such as logged-in navigation versus anonymous), returning results from flows, integration with Hilt, ViewModel, Kotlin, and view interoperability. license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-02' + last-updated: '2026-06-23' keywords: - recipe - Android @@ -33,6 +33,8 @@ metadata: - View interop. --- +*** ** * ** *** + ## Migration guide - *[Navigation 2 to Navigation 3 migration guide](references/android/guide/navigation/navigation-3/migration-guide.md)*: Step-by-step guide to migrate an Android application from Navigation 2 to Navigation 3, covering dependency updates, route changes, state management, and UI component replacements. @@ -43,7 +45,7 @@ metadata: ## Developer documentation -- \*[Navigation 3](references/android/guide/navigation/navigation-3/index.md). Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations. +- *[Navigation 3](references/android/guide/navigation/navigation-3/index.md)*. Search documentation for more information on basics, saving and managing navigation state, modularizing navigation code, creating custom layouts using Scenes, animating between destinations, or applying logic or wrappers to destinations. ## Recipes @@ -53,7 +55,7 @@ Code examples showcasing common patterns. - *[Basic](references/android/guide/navigation/navigation-3/recipes/basic.md)*: Shows most basic API usage. - *[Saveable back stack](references/android/guide/navigation/navigation-3/recipes/basicsaveable.md)*: Shows basic API usage with a persistent back stack. -- *[Entry provider DSL](references/android/guide/navigation/navigation-3/recipes/basicdsl.md)*: Shows basic API usage using the entryProvider DSL. +- *[Entry provider DSL](https://developer.android.com/guide/navigation/navigation-3/recipes/basicdsl)*: Shows basic API usage using the entryProvider DSL. ### Common UI @@ -61,7 +63,7 @@ Code examples showcasing common patterns. ### Deep links -- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. +- *[Basic](https://developer.android.com/guide/navigation/navigation-3/recipes/deeplinks-basic)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. - *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior. ### Scenes @@ -96,7 +98,7 @@ Code examples showcasing common patterns. ### Architecture - *[Modularized navigation code (Hilt)](references/android/guide/navigation/navigation-3/recipes/modular-hilt.md)*: Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI. -- *[Modularized navigation code (Koin)](references/android/guide/navigation/navigation-3/recipes/modular-koin.md)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI. +- *[Modularized navigation code (Koin)](https://developer.android.com/guide/navigation/navigation-3/recipes/modular-koin)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI. ### Working with ViewModel 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 deleted file mode 100644 index 2067c08..0000000 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md +++ /dev/null @@ -1,85 +0,0 @@ -# Basic DSL Recipe - -This recipe shows a basic example of how to use the Navigation 3 API with two screens, using the `entryProvider` DSL and a persistent back stack. - -## How it works - -This example is similar to the basic recipe, but with a few key differences: - -1. **Persistent Back Stack** : It uses `rememberNavBackStack(RouteA)` to create and remember the back stack. This makes the back stack persistent across configuration changes (e.g., screen rotation). To use `rememberNavBackStack`, the navigation keys must be serializable, which is why `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. - -2. **`entryProvider` DSL** : Instead of a `when` statement, this example uses the `entryProvider` DSL to define the content for each route. The `entry` function is used to associate a route type with its composable content. - -The navigation logic remains the same: to navigate from `RouteA` to `RouteB`, we add a `RouteB` instance to the back stack. -[![](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/basicdsl) - -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.nav3recipes.basicdsl - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.lifecycle.compose.dropUnlessResumed -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.content.ContentBlue -import com.example.nav3recipes.content.ContentGreen -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import kotlinx.serialization.Serializable - -@Serializable -private data object RouteA : NavKey - -@Serializable -private data class RouteB(val id: String) : NavKey - -class BasicDslActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - setContent { - val backStack = rememberNavBackStack(RouteA) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { - ContentGreen("Welcome to Nav3") { - Button(onClick = dropUnlessResumed { - backStack.add(RouteB("123")) - }) { - Text("Click to navigate") - } - } - } - entry { key -> - ContentBlue("Route id: ${key.id} ") - } - } - ) - } - } -} -``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md deleted file mode 100644 index 66312de..0000000 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md +++ /dev/null @@ -1,744 +0,0 @@ -# Deep Link Basic Recipe - -This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. - -## How it works - -It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. - -## Demonstrated forms of deeplink - -The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: - -1. `HomeKey` - deeplink with an exact url (no deeplink arguments) -2. `UsersKey` - deeplink with path arguments -3. `SearchKey` - deeplink with query arguments - -See `MainActivity.deepLinkPatterns` for the actual url pattern of each. - -## Recipe structure - -This recipe consists of three main packages: - -1. `basic.deeplink` - Contains the two activities -2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc -3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks - -[![](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/basic) - -``` -package com.example.nav3recipes.deeplink.basic - -import androidx.navigation3.runtime.NavKey -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS -import kotlinx.serialization.Serializable - -internal interface NavRecipeKey: NavKey { - val name: String -} - -@Serializable -internal object HomeKey: NavRecipeKey { - override val name: String = STRING_LITERAL_HOME -} - -@Serializable -internal data class UsersKey( - val filter: String, -): NavRecipeKey { - override val name: String = STRING_LITERAL_USERS - companion object { - const val FILTER_KEY = STRING_LITERAL_FILTER - const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" - const val FILTER_OPTION_ALL = "all" - } -} - -@Serializable -internal data class SearchKey( - val firstName: String? = null, - val ageMin: Int? = null, - val ageMax: Int? = null, - val location: String? = null, -): NavRecipeKey { - override val name: String = STRING_LITERAL_SEARCH -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.core.net.toUri -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FriendsList -import com.example.nav3recipes.common.deeplink.LIST_USERS -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT -import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher -import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern -import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest -import com.example.nav3recipes.deeplink.basic.util.KeyDecoder -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * Parses a target deeplink into a NavKey. There are several crucial steps involved: - * - * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable - * format (see [DeepLinkPattern]) - * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) - * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each - * other to facilitate comparison and finding a match - * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match - * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result - * to backstack key, regardless of what the conversion method may be. - * STEP 4. Associate the match results with the correct backstack key - * - * This recipes provides an example for each of the above steps by way of kotlinx.serialization. - * - * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional - * deeplink considerations are not included in this scope** - * - Create synthetic backStack - * - Multi-modular setup - * - DI - * - Managing TaskStack - * - Up button ves Back Button - * - */ -class MainActivity : ComponentActivity() { - /** STEP 1. Parse supported deeplinks */ - // internal so that landing activity can link to this in the kdocs - internal val deepLinkPatterns: List> = listOf( - // "https://www.nav3recipes.com/home" - DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), - // "https://www.nav3recipes.com/users/with/{filter}" - DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), - // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" - DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), - ) - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - // retrieve the target Uri - val uri: Uri? = intent.data - // associate the target with the correct backstack key - val key: NavKey = uri?.let { - /** STEP 2. Parse requested deeplink */ - val request = DeepLinkRequest(uri) - /** STEP 3. Compared requested with supported deeplink to find match*/ - val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> - DeepLinkMatcher(request, pattern).match() - } - /** STEP 4. If match is found, associate match to the correct key*/ - match?.let { - //leverage kotlinx.serialization's Decoder to decode - // match result into a backstack key - KeyDecoder(match.args) - .decodeSerializableValue(match.serializer) - } - } ?: HomeKey // fallback if intent.uri is null or match is not found - - /** - * Then pass starting key to backstack - */ - setContent { - val backStack: NavBackStack = rememberNavBackStack(key) - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { key -> - EntryScreen(key.name) { - TextContent("") - } - } - entry { key -> - EntryScreen("${key.name} : ${key.filter}") { - TextContent("") - val list = when { - key.filter.isEmpty() -> LIST_USERS - key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS - else -> LIST_USERS.take(5) - } - FriendsList(list) - } - } - entry { search -> - EntryScreen(search.name) { - TextContent("") - val matchingUsers = LIST_USERS.filter { user -> - (search.firstName == null || user.firstName == search.firstName) && - (search.location == null || user.location == search.location) && - (search.ageMin == null || user.age >= search.ageMin) && - (search.ageMax == null || user.age <= search.ageMax) - } - FriendsList(matchingUsers) - } - } - } - ) - } - } -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.core.net.toUri -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.EMPTY -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE -import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY -import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM -import com.example.nav3recipes.common.deeplink.LOCATION_BC -import com.example.nav3recipes.common.deeplink.LOCATION_BR -import com.example.nav3recipes.common.deeplink.LOCATION_CA -import com.example.nav3recipes.common.deeplink.LOCATION_US -import com.example.nav3recipes.common.deeplink.MenuDropDown -import com.example.nav3recipes.common.deeplink.MenuTextInput -import com.example.nav3recipes.common.deeplink.PaddedButton -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE -import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE -import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * This activity allows the user to create a deep link and make a request with it. - * - * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct - * and trigger the deeplink request, and the [MainActivity] to show how an app can handle - * that request. - * - * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to - * demonstrate different types of supported deeplinks: - * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) - * 2. [UsersKey] - deeplink with path arguments - * 3. [SearchKey] - deeplink with query arguments - * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. - * - * **RECIPE STRUCTURE** This recipe consists of three main packages: - * 1. basic.deeplink - Contains the two activities - * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc - * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match - * the deeplinks - * - * See [MainActivity] for how the requested deeplink is handled. - */ -class CreateDeepLinkActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - /** - * UI for deeplink sandbox - */ - EntryScreen("Sandbox - Build Your Deeplink") { - TextContent("Base url:\n${PATH_BASE}/") - var showFilterOptions by remember { mutableStateOf(false) } - val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } - - var showQueryOptions by remember { mutableStateOf(false) } - var selectedFilter by remember { mutableStateOf("") } - val selectedSearchQuery = remember { mutableStateMapOf() } - - // manage path options - MenuDropDown( - menuOptions = MENU_OPTIONS_PATH, - ) { _, selection -> - selectedPath.value = selection - when (selection) { - PATH_SEARCH -> { - showQueryOptions = true - showFilterOptions = false - } - - PATH_INCLUDE -> { - showQueryOptions = false - showFilterOptions = true - } - - else -> { - showQueryOptions = false - showFilterOptions = false - } - } - } - - // manage path filter options, reset state if menu is closed - LaunchedEffect(showFilterOptions) { - selectedFilter = if (showFilterOptions) { - MENU_OPTIONS_FILTER.values.first().first() - } else { - "" - } - } - if (showFilterOptions) { - MenuDropDown( - menuOptions = MENU_OPTIONS_FILTER, - ) { _, selected -> - selectedFilter = selected - } - } - - // manage query options, reset state if menu is closed - LaunchedEffect(showQueryOptions) { - if (showQueryOptions) { - val initEntry = MENU_OPTIONS_SEARCH.entries.first() - selectedSearchQuery[initEntry.key] = initEntry.value.first() - } else { - selectedSearchQuery.clear() - } - } - if (showQueryOptions) { - MenuTextInput( - menuLabels = MENU_LABELS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - MenuDropDown( - menuOptions = MENU_OPTIONS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - } - - // form final deeplink url - val arguments = when (selectedPath.value) { - PATH_INCLUDE -> "/${selectedFilter}" - PATH_SEARCH -> { - buildString { - selectedSearchQuery.forEach { entry -> - if (entry.value.isNotEmpty()) { - val prefix = if (isEmpty()) "?" else "&" - append("$prefix${entry.key}=${entry.value}") - } - } - } - } - - else -> "" - } - val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" - TextContent("Final url:\n$finalUrl") - // deeplink to target - PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { - val intent = Intent( - this@CreateDeepLinkActivity, - MainActivity::class.java - ) - // start activity with the url - intent.data = finalUrl.toUri() - startActivity(intent) - }) - } - } - } -} - -private const val KEY_PATH = "path" -private val MENU_OPTIONS_PATH = mapOf( - KEY_PATH to listOf( - STRING_LITERAL_HOME, - PATH_INCLUDE, - PATH_SEARCH, - ), -) - -private val MENU_OPTIONS_FILTER = mapOf( - UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), -) - -private val MENU_OPTIONS_SEARCH = mapOf( - SearchKey::firstName.name to listOf( - EMPTY, - FIRST_NAME_JOHN, - FIRST_NAME_TOM, - FIRST_NAME_MARY, - FIRST_NAME_JULIE - ), - SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) -) - -private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) - -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -/** - * Decodes the list of arguments into a a back stack key - * - * **IMPORTANT** This decoder assumes that all argument types are Primitives. - */ -@OptIn(ExperimentalSerializationApi::class) -internal class KeyDecoder( - private val arguments: Map, -) : AbstractDecoder() { - - override val serializersModule: SerializersModule = EmptySerializersModule() - private var elementIndex: Int = -1 - private var elementName: String = "" - - /** - * Decodes the index of the next element to be decoded. Index represents a position of the - * current element in the [descriptor] that can be found with [descriptor].getElementIndex. - * - * The returned index will trigger deserializer to call [decodeValue] on the argument at that - * index. - * - * The decoder continually calls this method to process the next available argument until this - * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more - * arguments to decode. - * - * This method should sequentially return the element index for every element that has its value - * available within [arguments]. - */ - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - var currentIndex = elementIndex - while (true) { - // proceed to next element - currentIndex++ - // if we have reached the end, let decoder know there are not more arguments to decode - if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE - val currentName = descriptor.getElementName(currentIndex) - // Check if bundle has argument value. If so, we tell decoder to process - // currentIndex. Otherwise, we skip this index and proceed to next index. - if (arguments.contains(currentName)) { - elementIndex = currentIndex - elementName = currentName - return elementIndex - } - } - } - - /** - * Returns argument value from the [arguments] for the argument at the index returned by - * [decodeElementIndex] - */ - override fun decodeValue(): Any { - val arg = arguments[elementName] - checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } - return arg - } - - override fun decodeNull(): Nothing? = null - - // we want to know if it is not null, so its !isNull - override fun decodeNotNullMark(): Boolean = arguments[elementName] != null -} -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri - -/** - * Parse the requested Uri and store it in a easily readable format - * - * @param uri the target deeplink uri to link to - */ -internal class DeepLinkRequest( - val uri: Uri -) { - /** - * A list of path segments - */ - val pathSegments: List = uri.pathSegments - - /** - * A map of query name to query value - */ - val queries = buildMap { - uri.queryParameterNames.forEach { argName -> - this[argName] = uri.getQueryParameter(argName)!! - } - } - - // TODO add parsing for other Uri components, i.e. fragments, mimeType, action -} -``` - -```` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.encoding.CompositeDecoder -import java.io.Serializable - -/** - * Parse a supported deeplink and stores its metadata as a easily readable format - * - * The following notes applies specifically to this particular sample implementation: - * - * The supported deeplink is expected to be built from a serializable backstack key [T] that - * supports deeplink. This means that if this deeplink contains any arguments (path or query), - * the argument name must match any of [T] member field name. - * - * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] - * supports two deeplink patterns: - * ``` - * val deeplink1 = www.nav3recipes.com/home - * val deeplink2 = www.nav3recipes.com/profile/{userId} - * ``` - * Then two [DeepLinkPattern] should be created - * ``` - * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) - * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) - * ``` - * - * This implementation assumes a few things: - * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match - * 2. all query arguments are optional by way of nullable/has default value - * - * @param T the backstack key type that supports the deeplinking of [uriPattern] - * @param serializer the serializer of [T] - * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" - */ -internal class DeepLinkPattern( - val serializer: KSerializer, - val uriPattern: Uri -) { - /** - * Help differentiate if a path segment is an argument or a static value - */ - private val regexPatternFillIn = Regex("\\{(.+?)\\}") - - // TODO make these lazy - /** - * parse the path into a list of [PathSegment] - * - * order matters here - path segments need to match in value and order when matching - * requested deeplink to supported deeplink - */ - val pathSegments: List = buildList { - uriPattern.pathSegments.forEach { segment -> - // first, check if it is a path arg - var result = regexPatternFillIn.find(segment) - if (result != null) { - // if so, extract the path arg name (the string value within the curly braces) - val argName = result.groups[1]!!.value - // from [T], read the primitive type of this argument to get the correct type parser - val elementIndex = serializer.descriptor.getElementIndex(argName) - if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { - throw IllegalArgumentException( - "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." - ) - } - - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - // finally, add the arg name and its respective type parser to the map - add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) - } else { - // if its not a path arg, then its just a static string path segment - add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) - } - } - } - - /** - * Parse supported queries into a map of queryParameterNames to [TypeParser] - * - * This will be used later on to parse a provided query value into the correct KType - */ - val queryValueParsers: Map = buildMap { - uriPattern.queryParameterNames.forEach { paramName -> - val elementIndex = serializer.descriptor.getElementIndex(paramName) - // Ignore static query parameters that are not in the Serializable class - if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - this[paramName] = getTypeParser(elementDescriptor.kind) - } - } - } - - /** - * Metadata about a supported path segment - */ - class PathSegment( - val stringValue: String, - val isParamArg: Boolean, - val typeParser: TypeParser - ) -} - -/** - * Parses a String into a Serializable Primitive - */ -private typealias TypeParser = (String) -> Serializable - -private fun getTypeParser(kind: SerialKind): TypeParser { - return when (kind) { - PrimitiveKind.STRING -> Any::toString - PrimitiveKind.INT -> String::toInt - PrimitiveKind.BOOLEAN -> String::toBoolean - PrimitiveKind.BYTE -> String::toByte - PrimitiveKind.CHAR -> String::toCharArray - PrimitiveKind.DOUBLE -> String::toDouble - PrimitiveKind.FLOAT -> String::toFloat - PrimitiveKind.LONG -> String::toLong - PrimitiveKind.SHORT -> String::toShort - else -> throw IllegalArgumentException( - "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." - ) - } -} -```` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.util.Log -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer - -internal class DeepLinkMatcher( - val request: DeepLinkRequest, - val deepLinkPattern: DeepLinkPattern -) { - /** - * Match a [DeepLinkRequest] to a [DeepLinkPattern]. - * - * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise - */ - fun match(): DeepLinkMatchResult? { - if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null - if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null - if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null - // exact match (url does not contain any arguments) - if (request.uri == deepLinkPattern.uriPattern) - return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) - - val args = mutableMapOf() - // match the path - request.pathSegments - .asSequence() - // zip to compare the two objects side by side, order matters here so we - // need to make sure the compared segments are at the same position within the url - .zip(deepLinkPattern.pathSegments.asSequence()) - .forEach { it -> - // retrieve the two path segments to compare - val requestedSegment = it.first - val candidateSegment = it.second - // if the potential match expects a path arg for this segment, try to parse the - // requested segment into the expected type - if (candidateSegment.isParamArg) { - val parsedValue = try { - candidateSegment.typeParser.invoke(requestedSegment) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) - return null - } - args[candidateSegment.stringValue] = parsedValue - } else if(requestedSegment != candidateSegment.stringValue){ - // if it's path arg is not the expected type, its not a match - return null - } - } - // match queries (if any) - request.queries.forEach { query -> - val name = query.key - // If the pattern does not define this query parameter, ignore it. - // This prevents a NullPointerException. - val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach - - val queryParsedValue = try { - queryStringParser.invoke(query.value) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) - return null - } - args[name] = queryParsedValue - } - // provide the serializer of the matching key and map of arg names to parsed arg values - return DeepLinkMatchResult(deepLinkPattern.serializer, args) - } -} - - -/** - * Created when a requested deeplink matches with a supported deeplink - * - * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink - * @param serializer serializer for [T] - * @param args The map of argument name to argument value. The value is expected to have already - * been parsed from the raw url string back into its proper KType as declared in [T]. - * Includes arguments for all parts of the uri - path, query, etc. - * */ -internal data class DeepLinkMatchResult( - val serializer: KSerializer, - val args: Map -) - -const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" -``` - -``` -package com.example.nav3recipes.deeplink.basic.ui - -import com.example.nav3recipes.deeplink.basic.SearchKey - -/** - * String resources - */ -internal const val STRING_LITERAL_FILTER = "filter" -internal const val STRING_LITERAL_HOME = "home" -internal const val STRING_LITERAL_USERS = "users" -internal const val STRING_LITERAL_SEARCH = "search" -internal const val STRING_LITERAL_INCLUDE = "include" -internal const val PATH_BASE = "https://www.nav3recipes.com" -internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" -internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" -internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" - -internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" -internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + - "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + - "&${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/modular-koin.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md deleted file mode 100644 index d1e7ad6..0000000 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md +++ /dev/null @@ -1,287 +0,0 @@ -# Modular Navigation Recipe (Koin) - -This recipe demonstrates how to structure a multi-module application using Navigation 3 and Koin for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. It relies on the [`koin-compose-navigation3`](https://insert-koin.io/docs/reference/koin-compose/navigation3) artifact. - -## How it works - -The application is divided into several Android modules: - -- **`app` module** : This is the main application module. It `includes()` the feature modules and initializes a common `Navigator`. - -- **`common` module** : This module contains the core navigation logic used by both the application module and the feature modules. Namely, it defines a `Navigator` class that manages the back stack. - -- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: - - - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. - - **`impl` module** : Provides the implementation of the feature, including its composables and Koin `Module`. The Koin module uses the [`navigation`](https://insert-koin.io/docs/reference/koin-compose/navigation3/#declaring-navigation-entries) DSL to define the entry provider installers for the feature module. - -This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. -[![](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/modular/koin) - -``` -package com.example.nav3recipes.modular.koin - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.dsl.module -import org.koin.dsl.navigation3.navigation - -// API -object Profile - -// IMPL -@OptIn(KoinExperimentalAPI::class) -val profileModule = module { - activityRetainedScope { - navigation { ProfileScreen() } - } -} - -@Composable -private fun ProfileScreen() { - val profileColor = MaterialTheme.colorScheme.surfaceVariant - Column( - modifier = Modifier - .fillMaxSize() - .background(profileColor) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Profile Screen", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.ui.theme.colors -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.dsl.module -import org.koin.dsl.navigation3.navigation - -// API -object ConversationList -data class ConversationDetail(val id: Int) { - val color: Color - get() = colors[id % colors.size] -} - -// IMPL -@OptIn(KoinExperimentalAPI::class) -val conversationModule = module { - activityRetainedScope { - navigation { - ConversationListScreen( - onConversationClicked = { conversationDetail -> - get().goTo(conversationDetail) - } - ) - } - - navigation { key -> - ConversationDetailScreen(key) { - get().goTo(Profile) - } - } - } -} - -@Composable -private fun ConversationListScreen( - onConversationClicked: (ConversationDetail) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(10) { index -> - val conversationId = index + 1 - val conversationDetail = ConversationDetail(conversationId) - val backgroundColor = conversationDetail.color - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = dropUnlessResumed { - onConversationClicked(conversationDetail) - }), - headlineContent = { - Text( - text = "Conversation $conversationId", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - colors = ListItemDefaults.colors( - containerColor = backgroundColor // Set container color directly - ) - ) - } - } -} - -@Composable -private fun ConversationDetailScreen( - conversationDetail: ConversationDetail, - onProfileClicked: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(conversationDetail.color) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Conversation Detail Screen: ${conversationDetail.id}", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = dropUnlessResumed(block = onProfileClicked)) { - Text("View Profile") - } - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList - -class Navigator(startDestination: Any) { - val backStack : SnapshotStateList = mutableStateListOf(startDestination) - - fun goTo(destination: Any){ - backStack.add(destination) - } - - fun goBack(){ - backStack.removeLastOrNull() - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import org.koin.androidx.scope.dsl.activityRetainedScope -import org.koin.dsl.module - -val appModule = module { - includes(profileModule,conversationModule) - - activityRetainedScope { - scoped { - Navigator(startDestination = ConversationList) - } - } -} -``` - -``` -package com.example.nav3recipes.modular.koin - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.ui.setEdgeToEdgeConfig -import org.koin.android.ext.android.inject -import org.koin.android.scope.AndroidScopeComponent -import org.koin.androidx.compose.navigation3.getEntryProvider -import org.koin.androidx.scope.activityRetainedScope -import org.koin.core.Koin -import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.core.component.KoinComponent -import org.koin.core.scope.Scope -import org.koin.dsl.koinApplication - -/** - * This recipe demonstrates how to use a modular approach with Navigation 3, - * where different parts of the application are defined in separate modules and injected - * into the main app using Koin. - * - * Features (Conversation and Profile) are split into two modules: - * - api: defines the public facing routes for this feature - * - impl: defines the entryProviders for this feature, these are injected into the app's main activity - * The common module defines: - * - a common navigator class that exposes a back stack and methods to modify that back stack - * - a type that should be used by feature modules to inject entryProviders into the app's main activity - * The app module creates the navigator by supplying a start destination and provides this navigator - * to the rest of the app module (i.e. MainActivity) and the feature modules. - */ -@OptIn(KoinExperimentalAPI::class) -class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComponent { - // Local Koin Context Instance - companion object { - private val localKoin = koinApplication { - modules(appModule) - }.koin - } - // Override default Koin context to use the local one - override fun getKoin(): Koin = localKoin - override val scope : Scope by activityRetainedScope() - val navigator: Navigator by inject() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setEdgeToEdgeConfig() - setContent { - Scaffold { paddingValues -> - NavDisplay( - backStack = navigator.backStack, - modifier = Modifier.padding(paddingValues), - onBack = { navigator.goBack() }, - entryProvider = getEntryProvider() - ) - } - } - } - -} -``` \ No newline at end of file diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 24b4fd1..2c9a9d7 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-06-09' + last-updated: '2026-06-19' keywords: - android - engage @@ -32,7 +32,7 @@ Follow these steps to assist the developer: - Always refer to [common.md](references/common.md) for common entities. - Ask which cluster type they want to publish from the supported cluster types for that vertical. - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. - - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. + - Get the request structure from [requests.md](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/requests) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. 2. **Generate Structured Boilerplate Code:** - Create a new directory for all Engage-related code. Name the directory to match the naming convention of the existing codebase. 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 fd2a79e..dea9db9 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 @@ -68,7 +68,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -294,6 +294,96 @@ There are following APIs to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| Food Reorder Cluster | `TYPE_FOOD_REORDER` | 5 | +| Food Shopping Cart Cluster | `TYPE_FOOD_SHOPPING_CART` | 6 | +| Food Shopping List Cluster | `TYPE_FOOD_SHOPPING_LIST` | 7 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> 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 6bf12d7..3d6a779 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 @@ -89,7 +89,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -343,6 +343,93 @@ There are following APIs to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> 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 8a4c22a..1bdb7c3 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 @@ -53,7 +53,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -363,14 +363,100 @@ APIs are available in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> if (task.isSuccessful) { // Handle IPC call success if(task.result) { - // Service is available on the device, proceed with content - // publish calls. + // Service is available on the device, proceed with content publish + // calls. } else { // Service is not available, no further action is needed. } 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 2e5ad1b..f1e7de9 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 @@ -129,7 +129,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -581,6 +581,102 @@ There are following APIs to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| Shopping Cart Cluster | `TYPE_SHOPPING_CART` | 4 | +| Food Reorder Cluster | `TYPE_FOOD_REORDER` | 5 | +| Food Shopping Cart Cluster | `TYPE_FOOD_SHOPPING_CART` | 6 | +| Food Shopping List Cluster | `TYPE_FOOD_SHOPPING_LIST` | 7 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Shopping List Cluster | `TYPE_SHOPPING_LIST` | 9 | +| Shopping Reorder Cluster | `TYPE_SHOPPING_REORDER` | 10 | +| Shopping Order Tracking Cluster | `TYPE_SHOPPING_ORDER_TRACKING` | 11 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | +| Continue Search Cluster | `TYPE_CONTINUE_SEARCH` | 13 | +| Reservation Cluster | `TYPE_RESERVATION` | 14 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> 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 d1abfb8..ccd4514 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 @@ -55,7 +55,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -225,14 +225,100 @@ APIs are available in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> if (task.isSuccessful) { // Handle IPC call success if(task.result) { - // Service is available on the device, proceed with content - // publish calls. + // Service is available on the device, proceed with content publish + // calls. } else { // Service is not available, no further action is needed. } 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 cc79de9..4e24895 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 @@ -89,7 +89,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } For more information, see [Package visibility in Android @@ -263,6 +263,97 @@ Following APIs are exposed to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| Shopping Cart Cluster | `TYPE_SHOPPING_CART` | 4 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Shopping List Cluster | `TYPE_SHOPPING_LIST` | 9 | +| Shopping Reorder Cluster | `TYPE_SHOPPING_REORDER` | 10 | +| Shopping Order Tracking Cluster | `TYPE_SHOPPING_ORDER_TRACKING` | 11 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> @@ -286,8 +377,8 @@ whether the content can be presented on the device. if (task.isSuccessful()) { // Handle success if(task.getResult()) { - // Service is available on the device, proceed with content - // publish calls. + // Service is available on the device, proceed with content publish + // calls. } else { // Service is not available, no further action is needed. } 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 def7f16..b1e8f78 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 @@ -69,7 +69,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -197,14 +197,90 @@ There are following APIs to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> if (task.isSuccessful) { // Handle IPC call success if(task.result) { - // Service is available on the device, proceed with content - // publish calls. + // Service is available on the device, proceed with content publish + // calls. } else { // Service is not available, no further action is needed. } @@ -220,8 +296,8 @@ whether the content can be presented on the device. if (task.isSuccessful()) { // Handle success if(task.getResult()) { - // Service is available on the device, proceed with content - // publish calls. + // Service is available on the device, proceed with content publish + // calls. } else { // Service is not available, no further action is needed. } 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 b563ae3..d9a3507 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 @@ -131,7 +131,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### Summary @@ -611,6 +611,94 @@ There are following APIs to publish clusters in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | +| Continue Search Cluster | `TYPE_CONTINUE_SEARCH` | 13 | +| Reservation Cluster | `TYPE_RESERVATION` | 14 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> @@ -1044,8 +1132,8 @@ 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. +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. 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 83c50aa..2afe8d9 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 @@ -26,7 +26,7 @@ Before you begin, complete the following steps: ### Mobile dependencies { - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } ### TV 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 d910945..3dc495e 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 @@ -61,7 +61,7 @@ Add the `com.google.android.engage:engage-core` library to your app: dependencies { // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.5.12' + implementation 'com.google.android.engage:engage-core:1.6.0' } For more information, see [Package visibility in Android @@ -369,6 +369,92 @@ APIs are available in the client: 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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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. + +##### For SDK versions prior to v1.6.0 (Deprecated) + ### Kotlin client.isServiceAvailable.addOnCompleteListener { task -> diff --git a/play/engage-sdk-integration/references/requests.md b/play/engage-sdk-integration/references/requests.md deleted file mode 100644 index c65f0a6..0000000 --- a/play/engage-sdk-integration/references/requests.md +++ /dev/null @@ -1,231 +0,0 @@ -This file defines the request structures for publishing various data models in -the Engage SDK. - - { - "PublishRecommendationClustersRequest": { - "package": "com.google.android.engage.service.PublishRecommendationClustersRequest", - "fields": { - "recommendationClusters": { - "type": "List", - "requirement": "Required", - "adder": "addRecommendationCluster(RecommendationCluster)", - "getter": "getRecommendationClusters()" - }, - "accountProfile": { - "type": "@NonNull AccountProfile", - "requirement": "Optional", - "setter": "setAccountProfile(@NonNull AccountProfile)", - "getter": "getAccountProfile()" - }, - "syncAcrossDevices": { - "type": "Boolean", - "requirement": "Optional", - "setter": "setSyncAcrossDevices(boolean)", - "getter": "getSyncAcrossDevices()" - } - } - }, - "PublishFeaturedClusterRequest": { - "package": "com.google.android.engage.service.PublishFeaturedClusterRequest", - "fields": { - "featuredCluster": { - "type": "FeaturedCluster", - "requirement": "Required", - "setter": "setFeaturedCluster(FeaturedCluster)", - "getter": "getFeaturedCluster()" - } - } - }, - "DeleteClustersRequest": { - "package": "com.google.android.engage.service.DeleteClustersRequest", - "fields": { - "clusterTypes": { - "type": "List<@ClusterType int>", - "requirement": "Optional", - "adder": "addClusterType(@ClusterType int)" - }, - "deleteReason": { - "type": "@DeleteReason int", - "requirement": "Optional", - "setter": "setDeleteReason(@DeleteReason int)", - "getter": "getDeleteReason()" - }, - "accountProfile": { - "requirement": "Optional", - "setter": "setAccountProfile(AccountProfile)", - "type": "AccountProfile", - "getter": "getAccountProfile()" - }, - "syncAcrossDevices": { - "requirement": "Optional", - "setter": "setSyncAcrossDevices(boolean)", - "type": "Boolean", - "getter": "getSyncAcrossDevices()" - } - } - }, - "PublishContinuationClusterRequest": { - "package": "com.google.android.engage.service.PublishContinuationClusterRequest", - "fields": { - "continuationCluster": { - "type": "ContinuationCluster", - "requirement": "Required", - "setter": "setContinuationCluster(ContinuationCluster)", - "getter": "getContinuationCluster()" - } - } - }, - "PublishStatusRequest": { - "package": "com.google.android.engage.service.PublishStatusRequest", - "fields": { - "statusCode": { - "type": "@AppEngagePublishStatusCode int", - "requirement": "Required", - "setter": "setStatusCode(@AppEngagePublishStatusCode int)", - "getter": "getStatusCode()" - } - } - }, - "PublishSubscriptionRequest": { - "package": "com.google.android.engage.service.PublishSubscriptionRequest", - "fields": { - "subscriptionClusters": { - "type": "List", - "requirement": "Required" - }, - "accountProfile": { - "type": "AccountProfile", - "requirement": "Required", - "setter": "setAccountProfile(AccountProfile)", - "getter": "getAccountProfile()" - }, - "subscription": { - "requirement": "Required", - "setter": "setSubscription(SubscriptionEntity)", - "type": "SubscriptionEntity", - "getter": "getSubscription()" - } - } - }, - "PublishUserAccountManagementRequest": { - "package": "com.google.android.engage.service.PublishUserAccountManagementRequest", - "fields": { - "actionUri": { - "type": "Uri", - "requirement": "Required" - }, - "signInCardEntity": { - "type": "SignInCardEntity", - "requirement": "Required", - "setter": "setSignInCardEntity(SignInCardEntity)" - }, - "userSettingsCardEntity": { - "requirement": "Required", - "setter": "setUserSettingsCardEntity(UserSettingsCardEntity)", - "type": "UserSettingsCardEntity" - } - } - }, - "PublishShoppingCartClusterRequest": { - "package": "com.google.android.engage.shopping.service.PublishShoppingCartClusterRequest", - "fields": { - "shoppingCart": { - "requirement": "Required", - "setter": "setShoppingCart(ShoppingCart)", - "type": "ShoppingCart", - "getter": "getShoppingCart()" - } - } - }, - "PublishShoppingListsRequest": { - "package": "com.google.android.engage.shopping.service.PublishShoppingListsRequest", - "fields": { - "shoppingLists": { - "type": "List", - "requirement": "Optional", - "adder": "addShoppingList(ShoppingList)", - "getter": "getShoppingLists()", - "adderAll": "addShoppingLists(List)" - } - } - }, - "PublishShoppingOrderTrackingClusterRequest": { - "package": "com.google.android.engage.shopping.service.PublishShoppingOrderTrackingClusterRequest", - "fields": { - "shoppingOrderTrackingCluster": { - "type": "ShoppingOrderTrackingCluster", - "requirement": "Required", - "setter": "setShoppingOrderTrackingCluster(ShoppingOrderTrackingCluster)", - "getter": "getShoppingOrderTrackingCluster()" - } - } - }, - "PublishShoppingReorderClusterRequest": { - "package": "com.google.android.engage.shopping.service.PublishShoppingReorderClusterRequest", - "fields": { - "reorderCluster": { - "type": "ShoppingReorderCluster", - "requirement": "Required", - "setter": "setReorderCluster(ShoppingReorderCluster)", - "getter": "getReorderCluster()" - } - } - }, - "PublishFoodShoppingCartsRequest": { - "package": "com.google.android.engage.food.service.PublishFoodShoppingCartsRequest", - "fields": { - "foodShoppingCarts": { - "type": "List", - "requirement": "Optional", - "adder": "addFoodShoppingCart(FoodShoppingCart)", - "getter": "getFoodShoppingCarts()", - "adderAll": "addFoodShoppingCarts(List)" - } - } - }, - "PublishFoodShoppingListsRequest": { - "package": "com.google.android.engage.food.service.PublishFoodShoppingListsRequest", - "fields": { - "foodShoppingLists": { - "type": "List", - "requirement": "Optional", - "adder": "addFoodShoppingList(FoodShoppingList)", - "getter": "getFoodShoppingLists()", - "adderAll": "addFoodShoppingLists(List)" - } - } - }, - "PublishReorderClusterRequest": { - "package": "com.google.android.engage.food.service.PublishReorderClusterRequest", - "fields": { - "reorderCluster": { - "requirement": "Required", - "setter": "setReorderCluster(FoodReorderCluster)", - "type": "FoodReorderCluster", - "getter": "getReorderCluster()" - } - } - }, - "PublishContinueSearchClusterRequest": { - "package": "com.google.android.engage.travel.service.PublishContinueSearchClusterRequest", - "fields": { - "continueSearchCluster": { - "type": "ContinueSearchCluster", - "requirement": "Required", - "setter": "setContinueSearchCluster(ContinueSearchCluster)", - "getter": "getContinueSearchCluster()" - } - } - }, - "PublishReservationClusterRequest": { - "package": "com.google.android.engage.travel.service.PublishReservationClusterRequest", - "fields": { - "reservationCluster": { - "type": "ReservationCluster", - "requirement": "Required", - "setter": "setReservationCluster(ReservationCluster)", - "getter": "getReservationCluster()" - } - } - } - } \ 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 4db8953..83392ea 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-06-02' + last-updated: '2026-06-19' keywords: - android - play billing 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 c1106b1..462f373 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,5 +1,26 @@ 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. + +## Google Play Billing Library 9.1.0 Release (2026-06-18) + +Version 9.1.0 of the Google Play Billing Library and Kotlin extensions are now available. + +### Summary of changes + +- New APIs for [Billing Choice](https://developer.android.com/google/play/billing/billingchoice): + - Added [`BillingClient.getBillingChoiceInfoAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getBillingChoiceInfoAsync(com.android.billingclient.api.GetBillingChoiceInfoParams,com.android.billingclient.api.BillingChoiceInfoResponseListener)) to retrieve information about billing choices available to the user. + - Added [`BillingChoiceInfo`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfo) class which contains the response details, including image URLs and loyalty information. + - Added [`GetBillingChoiceInfoParams`](https://developer.android.com/reference/com/android/billingclient/api/GetBillingChoiceInfoParams) to configure the request. + - Added [`BillingChoiceInfoResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfoResponseListener) to receive the callback. + - Added Kotlin extension [`BillingClient.getBillingChoiceInfo()`](https://developer.android.com/reference/kotlin/com/android/billingclient/api/package-summary#(com.android.billingclient.api.BillingClient).getBillingChoiceInfo(com.android.billingclient.api.GetBillingChoiceInfoParams)) suspend function. + - Added [`BillingClient.showBillingProgramInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showBillingProgramInformationDialog(android.app.Activity,com.android.billingclient.api.BillingProgramInformationDialogParams,com.android.billingclient.api.BillingProgramInformationDialogListener)) to show an information dialog for a billing program. + - Added [`BillingProgramInformationDialogParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogParams) to configure the dialog. + - Added [`BillingProgramInformationDialogListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogListener) to receive the callback. + - Added [`BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails) to provide details about billing choice availability. + - Added [`ChoiceScreenType`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType) to specify the type of choice screen. + ## 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 diff --git a/security/android-intent-security/SKILL.md b/security/android-intent-security/SKILL.md new file mode 100644 index 0000000..ac09e4a --- /dev/null +++ b/security/android-intent-security/SKILL.md @@ -0,0 +1,554 @@ +--- +name: android-intent-security +description: Best practices for Android Intent security. Use this skill when auditing + component configurations in AndroidManifest.xml activities, services, receivers) + or source code handling incoming Intents (getIntent, getParcelableExtra) to prevent + Intent Redirection and unauthorized access. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-06-24' + keywords: + - recipe + - Android + - Security + - Intent + - Redirection + - PendingIntent + - ContentProvider + - Service + - Signature + - Verification + - sanitizer + - Vulnerability + - Best Practices +--- + +auditing component configurations in AndroidManifest.xml (activities, services, +receivers) or source code handling incoming Intents (getIntent, +getParcelableExtra) to prevent Intent Redirection and unauthorized access. +keywords_public: recipe, Android, Security, Intent, Redirection, PendingIntent, +ContentProvider, Service ,Signature, Verification, Sanitizer, Vulnerability, +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. + +## Glossary + +- **Intent:** An asynchronous messaging object used to request an action from another app component. +- **Exported component:** A component (`android:exported="true"`) that can be launched by other apps on the device. +- **Sticky intent:** A broadcast intent that remains in the system cache after it's sent, allowing any app to retrieve its contents. +- **Signature permission:** A permission whose protection level is set to `signature`, granted only to apps signed with the same developer key. +- **onNewIntent:** An activity lifecycle callback invoked when an activity is launched with `FLAG_ACTIVITY_SINGLE_TOP` and is already running at the top of the history stack. +- **PendingIntent:** A token granted to a foreign application (for example, system services) allowing it to execute a predefined Intent with the creator's permissions. +- **Mutable PendingIntent:** A PendingIntent whose underlying Intent parameters can be modified by the receiving application. +- **ContentProvider:** A component that encapsulates data and provides it to other applications via standard query/insert interfaces. +- **IntentSanitizer:** A utility class in AndroidX Core used to build a safe, sanitized copy of an incoming Intent by filtering out unauthorized components, actions, or extras. +- **Intent redirection (forwarding):** A vulnerability where an application receives an intent from an untrusted source and uses it to launch a private, non-exported component. + +## Prerequisites + +- The agent **MUST** be able to describe the function and security implications of `onCreate`, `onNewIntent`, and the `singleTop` launch mode. +- The agent **MUST** be able to declare ``, ``, ``, and `` tags in `AndroidManifest.xml` and define their `android:exported` and `android:permission` attributes. +- The agent **MUST** be able to implement signature verification checks using `PackageManager`. + +## Limitations + +- This skill focuses on local inter-component and inter-app communication security on the Android platform. +- This skill **doesn't** cover network security, web integration, or host-to-server security. + +## Setup and dependencies + +- **Android SDK:** Minimum API Level 23 (Android 6.0) is required for standard hardware-backed keystore operations and component validation. +- **AndroidX Core Library:** `androidx.core:core:1.9.0` or higher is **mandatory** to leverage `IntentSanitizer`. +- **Standard API access:** Standard Android `PackageManager` APIs are required for runtime component verification. + +*** ** * ** *** + +## Intent security logic and decisions + +### 1. Intent routing comparison + +Evaluate the security features of different intent delivery methods: + +| Intent Delivery Method | Scope | Recommended Use Case | +|---|---|---| +| Explicit Intent (Internal) | App Private | Launching internal activities/services | +| Implicit Intent | System Wide | Launching system camera, dialer, or sharing | +| Local Broadcasts (LocalBroadcastManager) (DEPRECATED) | App Private | Internal asynchronous event routing. **Deprecated**: Use in-app observers like Kotlin Flows/SharedFlow, LiveData, or reactive patterns instead. | +| System Broadcasts | System Wide | Receiving system events (NFC, Bluetooth) | + +### 2. PendingIntent mutability flag options + +Evaluate the security implications of PendingIntent mutability flags: + +| Flag Name | Mutability | Recommended Use Case | +|---|---|---| +| `PendingIntent.FLAG_IMMUTABLE` | Immutable | Default for almost all PendingIntents, such as alarms and notifications | +| `PendingIntent.FLAG_MUTABLE` | Mutable | Inline notifications replies, slice actions (requires explicit target intent) | + +### 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. +} + +### 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`. +} +} + +### 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. +} + +### 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. +} + +*** ** * ** *** + +## Code and configuration patterns + +### 1. Safe intent redirection (manual verification) + +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() { + val nestedIntent = IntentCompat.getParcelableExtra(intent, "EXTRA_NESTED_INTENT", Intent::class.java) + if (nestedIntent != null) { + // 1. Check for URI permission grants to prevent URI permission bypass + val hasUriPermissionGrants = ( + nestedIntent.flags and ( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + ) != 0 + if (hasUriPermissionGrants) { + throw SecurityException("Nested intent contains forbidden URI permission grant flags!") + } + + val pm = packageManager + val target = nestedIntent.resolveActivity(pm) + if (target != null) { + // 2. Verify target is within the same package + if (target.packageName != packageName) { + throw SecurityException("Cross-app intent redirection is forbidden!") + } + try { + // 3. Verify target activity is exported + val info = pm.getActivityInfo(target, 0) + if (!info.exported) { + throw SecurityException("Target activity is private: ${target.className}") + } + // 4. Explicitly set the component to prevent intent interception + nestedIntent.component = target + // Safe to launch + startActivity(nestedIntent) + } catch (e: PackageManager.NameNotFoundException) { + Log.e("Security", "Failed to resolve target activity", e) + } + } + } +} +``` + +
+ +### 2. Safe intent redirection using IntentSanitizer + +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() { + val untrustedIntent = IntentCompat.getParcelableExtra(intent, "EXTRA_NESTED_INTENT", Intent::class.java) + if (untrustedIntent != null) { + // Define the strict boundaries for allowed redirection target + val sanitizer = IntentSanitizer.Builder() + .allowComponent(ComponentName("com.example.app", "com.example.app.SafeTargetActivity")) // Explicitly allowed target + .allowAction(Intent.ACTION_VIEW) // Explicitly allowed actions + .allowDataWithAuthority("com.example.app.provider") // Allowed URI authority + .allowType("text/plain") // Allowed mime type + .allowExtra("user_display_name", String::class.java) // Safe type-enforced extras + // Note: URI permission flags are NOT allowed, so the sanitizer will automatically strip or throw on them + .build() + + try { + // Option A: Throws SecurityException if the intent violates policies + val safeIntent = sanitizer.sanitizeByThrowing(untrustedIntent) + startActivity(safeIntent) + } catch (e: SecurityException) { + Log.e("SECURITY_ALERT", "Attempted launch of non-allowlisted intent blocked", e) + } + + // Option B: Silently filter and launch only the authorized parts (no exception thrown) + // val filteredIntent = sanitizer.sanitizeByFiltering(untrustedIntent) + // startActivity(filteredIntent) + } +} +``` + +
+ +### 3. Custom signature permission protection + +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`. + +- **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) { + super.onNewIntent(newIntent) + + // Set the intent to ensure intent returns the new one + intent = newIntent + + // Validate the intent payload + if (validateIntent(newIntent)) { + processIntentPayload(newIntent) + } else { + Log.w("SECURITY_ALERT", "Received invalid or insecure intent during warm boot") + } +} + +private fun validateIntent(intent: Intent): Boolean { + return intent.hasExtra("VALID_PAYLOAD_MARKER") +} +``` + +
+ +### 5. Secure PendingIntent creation + +Enforce immutability unless mutability is explicitly required. + +- **Expected Inputs (Immutable):** An intent target. +- **Expected Outputs (Immutable):** A `PendingIntent` that cannot be altered by the receiver. +- **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) { + // 1. Secure Immutable PendingIntent (Default) + val intent = Intent(context, TargetActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + // 2. Secure Mutable PendingIntent (e.g., Notification Direct Reply) + val mutableIntent = Intent().apply { + // MUST set explicit target component to prevent redirection hijacking + component = ComponentName(context, ReplyReceiver::class.java) + } + val mutablePendingIntent = PendingIntent.getBroadcast( + context, + 0, + mutableIntent, + 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. + +- **Expected Inputs:** + - `uri` (`Uri`): The query URI. + - `projection` (`String[]`): Columns to retrieve. + - `selection` (`String`): Query criteria. + - `selectionArgs` (`String[]`): Values mapping to selection placeholders (`?`). +- **Expected Outputs:** + - `Cursor`: Filtered query results, strictly bound to projection maps. + + +```xml + +``` + +
+ + +```kotlin +override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? +): Cursor? { + val queryBuilder = SQLiteQueryBuilder() + queryBuilder.tables = tableName + // Strict projection map to prevent querying unauthorized columns + queryBuilder.projectionMap = mapOf( + "_id" to "_id", + "display_name" to "display_name" + ) + // Enable strict validation (always available since minSdk is 36) + queryBuilder.setStrict(true) + queryBuilder.setStrictColumns(true) + queryBuilder.setStrictGrammar(true) + + // MUST parameterize selection criteria; NEVER append selection strings directly + val db = dbHelper.readableDatabase + return queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder) +} +``` + +
+ +### 7. Service caller signature verification + +Verify the calling application's signature before binding to a service. + +- **Expected Inputs:** + - `intent` (`Intent`): The binding request intent. +- **Expected Outputs:** + - `IBinder`: Local binder instance if caller signature matches trusted partner; throws `SecurityException` otherwise. + + +```kotlin +class SecureBoundService : Service() { + companion object { + // Expected SHA-256 hash of the trusted app's signing certificate (Base64 encoded) + private const val TRUSTED_PARTNER_SHA256 = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V=" + } + + override fun onBind(intent: Intent): IBinder { + // Return the binder. Do NOT perform signature verification in onBind() because + // the binder connection is cached by Android, which can bypass checks on subsequent binds. + return LocalBinder() + } + + private fun enforceTrustedCaller() { + val callingUid = Binder.getCallingUid() + // Allow calls from the same application + if (callingUid == Process.myUid()) { + return + } + val pm = packageManager + val packages = pm.getPackagesForUid(callingUid) + + if (packages.isNullOrEmpty() || !verifySignature(pm, packages[0])) { + throw SecurityException("Access Denied: Caller signature is untrusted.") + } + } + + private fun verifySignature(pm: PackageManager, packageName: String): Boolean { + try { + val trustedSha256Raw = Base64.decode(TRUSTED_PARTNER_SHA256, Base64.DEFAULT) + // API 28+ handles rotated certificates and avoids manual hashing. + // Since minSdk is 36, this is always available. + return pm.hasSigningCertificate(packageName, trustedSha256Raw, PackageManager.CERT_INPUT_SHA256) + } catch (e: Exception) { + Log.e("SECURITY_ERROR", "Verification failed for package: $packageName", e) + } + return false + } + + inner class LocalBinder : Binder() { + fun doSecureWork() { + // Verify caller identity on every transaction method call + enforceTrustedCaller() + // Safe to proceed with sensitive operations + } + } +} +``` + +
+ +*** ** * ** *** + +## Error handling + +Handle component binding, database queries, and intent redirection failures +securely to avoid exposing internal structures. + + +```kotlin +fun safeErrorHandling(callingPackage: String?) { + try { + val payload = intent.getStringExtra("DATA_EXTRA") ?: throw IllegalArgumentException("Payload parameter missing.") + // Create a specific target intent using the validated payload + val targetIntent = Intent(this, TargetActivity::class.java).apply { + putExtra("SECURE_PAYLOAD", payload) + } + startActivity(targetIntent) + } catch (e: SecurityException) { + // MUST log security violations for audit, but NEVER expose exception details to the user. + Log.e("SECURITY_ERROR", "Unauthorized component transition blocked. Calling Package: ${callingPackage ?: "Unknown"}", e) + // MUST provide generic user feedback. + showFeedbackToUser("Process request failed: Access Denied.") + } catch (e: IllegalArgumentException) { + Log.w("INTEGRITY_WARNING", "Missing intent parameter", e) + } +} +``` + +
+ + // Secure handling of ContentProvider queries on the client side: + try { + val cursor = contentResolver.query(providerUri, projection, selection, selectionArgs, null) + } catch (e: SQLiteException) { + Log.e("PROVIDER_ERROR", "ContentProvider database query failed", e) + // Secure handling: prevent raw query syntax details from leaking to UI + } + +*** ** * ** *** + +## 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: + +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). +3. **Scope of changes:** A list of all modified classes, XML files, and dependencies. +4. **Implementation summary:** Concrete details of the solution (for example, "Updated nested intent parsing to use the `IntentSanitizer` API with a strict component allowlist"). +5. **Code diff:** Standard unified diffs showing the exact modifications. + +### Best practices and security alignment update template + +Use the following markdown template when reporting changes to developers: + + ### Best practices and security alignment update: [Security Alignment Area] + + * **Improvement Description:** [Brief description of the hardening update and why it's recommended] + * **Priority Level:** [High / Medium / Low] + * **Alignment Action:** [Summary of updates, for example, converted to FLAG_IMMUTABLE] + + #### Files modified + * `[Relative path to File 1]` + * `[Relative path to File 2]` + + #### Implementation diff + ```diff + // Insert Unified Diff here + +#### Testing and verification + +1. \[Step 1 to verify the component behaves correctly, for example, run component unit test\] +2. \[Step 2 to verify regression safety\] \`\`\` + +*** ** * ** *** + +## Antipatterns + +- **NEVER** launch a nested `Intent` received from an untrusted source without verifying its target package and exported status. +- **NEVER** use sticky broadcasts (`sendStickyBroadcast`). +- **NEVER** assume an exported component is safe because it runs in a background thread or performs internal checks. +- **NEVER** expose sensitive functionalities (like SSO authentication or payment processors) to components without signature-level permission restrictions. +- **NEVER** process incoming intents in `onNewIntent` without applying the same security controls as `onCreate`. +- **NEVER** create a mutable `PendingIntent` without setting an explicit target component in the base `Intent`. +- **NEVER** use dynamic string concatenation to construct selection blocks inside a `ContentProvider` query. +- **NEVER** use `Binder.getCallingUid` inside a `BroadcastReceiver.onReceive` to identify the sender of a broadcast, as it returns the receiver's own UID, not the sender's. + +## Best Practices + +- **MUST** explicitly set `android:exported="false"` for all components that don't need external communication. +- **MUST** protect all exported components with custom permissions utilizing `android:protectionLevel="signature"` when communicating between family apps. +- **MUST** validate all incoming intent extras and handle missing parameters gracefully to prevent crashes. +- **MUST** rely on the system's **Protected Broadcast** mechanism for system events (for example, boot completed, package changes), as the system prevents untrusted apps from spoofing these actions. +- **MUST** protect custom broadcasts with signature-level permissions or use `RECEIVER_NOT_EXPORTED` for dynamic receivers to restrict the sender identity. +- **MUST** call `setIntent(newIntent)` inside `onNewIntent()` before processing payloads to keep active references updated. +- **MUST** use `PendingIntent.FLAG_IMMUTABLE` by default when constructing `PendingIntent` instances. +- **MUST** protect exported `ContentProviders` with `readPermission` and `writePermission`. +- **MUST** enforce parameterized selection structures in `ContentProvider` query/update methods. +- **MUST** verify the package signature fingerprint of binding applications at runtime inside exported services. +- **MUST** use `androidx.core.content.IntentSanitizer` to sanitize incoming dynamic intents before redirection, if AndroidX Core 1.9.0+ is imported in the project. diff --git a/wear/jetpack-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md similarity index 90% rename from wear/jetpack-compose-m3/SKILL.md rename to wear/wear-compose-m3/SKILL.md index 84b8be2..861d2c4 100644 --- a/wear/jetpack-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -1,5 +1,5 @@ --- -name: jetpack-compose-m3 +name: wear-compose-m3 description: Expert guidance for working with Wear OS Compose Material3. Use this skill when creating, updating or migrating Wear OS projects. This includes the androidx.wear.compose.material3, androidx.wear.compose.foundation and androidx.wear.compose.navigation3 libraries. @@ -8,7 +8,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-06' + last-updated: '2026-06-18' keywords: - Wear OS - Compose @@ -75,33 +75,32 @@ before proceeding. #### Step 2: Check the local cache -1. Define the cache directory path: `/samples/{VERSION}/`. Do NOT choose your own different location. +1. Define the cache directory path: `/tmp/wear-compose-samples/{VERSION}/`. Do NOT choose your own different location. 2. Check if this directory exists and contains subdirectories with `.kt` files. - **IF YES (cache hit):** Proceed to **Step 4**. - **IF NO (cache miss):** Proceed to **Step 3**. -#### Step 3: Check the Gradle cache +#### Step 3: Network-based sample retrieval -1. Sample sources are stored in the Gradle cache. To avoid slow, brute-force searches: - - Determine the Gradle user home (usually `~/.gradle`, or check `$GRADLE_USER_HOME`). - - The cache root is `/caches`. Call this ``. -2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include "navigation3" in the list if the `androidx.wear.compose.navigation3` library is being used. -3. For each `{ARTIFACT}` in the list: +Google Maven publishes a `-samples-sources.jar` alongside every library release. +Download and extract it directly without needing a local Gradle sync or cache +lookup. - - Construct the expected relative path segment for the library: `androidx.wear.compose/compose-{ARTIFACT}/{VERSION}`. - - Run a targeted `find` command. Here is an example which is constructed - for efficiency: +1. Determine the `{VERSION}` of `androidx.wear.compose:compose-material3` from `build.gradle.kts` or `libs.versions.toml`. +2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include `navigation3` if `androidx.wear.compose.navigation3` is used. +3. For each `{ARTIFACT}`, run the following commands to download and extract + the samples: - find /modules-2/files-2.1/androidx.wear.compose/compose-{ARTIFACT}/{VERSION}/ \ - -name "*samples-sources.jar" + # Download the samples-sources JAR + curl -sSL "https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-{ARTIFACT}/{VERSION}/compose-{ARTIFACT}-{VERSION}-samples-sources.jar" -o {ARTIFACT}-{VERSION}-samples.jar -4. Use this JAR as the official sample sources. + # Extract and flatten into the cache directory + unzip -q -o {ARTIFACT}-{VERSION}-samples.jar -d /tmp/wear-compose-samples/{VERSION}/{ARTIFACT}/ -5. Extract the contents of each JAR to - `/samples/{VERSION}/{ARTIFACT}/` using `unzip -j` to flatten the - structure. + # Clean up + rm {ARTIFACT}-{VERSION}-samples.jar -6. Proceed **directly to step 4**. +4. If this works, proceed directly to step 4. #### Step 4: Read samples and implement diff --git a/wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md similarity index 100% rename from wear/jetpack-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md rename to wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index b1dab31..449394c 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-09' + last-updated: '2026-06-23' keywords: - Jetpack Compose Glimmer - audio glasses @@ -207,7 +207,7 @@ If you are creating a Glimmer Button component, read the: - **Developer Guidance:** [Jetpack Compose Glimmer: Buttons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md) - **API Source Code:** Use [references/button-source.md](references/button-source.md). -- **Implementation Samples:** Use [references/button-samples-source.md](references/button-samples-source.md). +- **Implementation Samples:** Use [references/button-samples-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source). #### Title Chips @@ -277,7 +277,7 @@ If you are creating a Glimmer List component, read the: - **API Source Code (List):** Use [references/list-source.md](references/list-source.md). - **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). -- **API Source Code (ListState):** Use [references/liststate-source.md](references/liststate-source.md). +- **API Source Code (ListState):** Use [references/liststate-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source). #### Stacks diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md deleted file mode 100644 index fba367d..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md +++ /dev/null @@ -1,176 +0,0 @@ -When creating a Glimmer Button component, refer to the following implementation -samples in `ButtonSamples.kt`: - - -```kotlin -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.xr.glimmer.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.xr.glimmer.Button -import androidx.xr.glimmer.ButtonSize -import androidx.xr.glimmer.GlimmerTheme -import androidx.xr.glimmer.Icon -import androidx.xr.glimmer.Text -import androidx.xr.glimmer.list.GlimmerLazyColumn - -@Composable -fun ButtonSampleUsage() { - GlimmerLazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), - ) { - item { ButtonSample() } - item { ButtonWithLeadingIconSample() } - item { ButtonWithTrailingIconSample() } - item { ButtonWithLeadingAndTrailingIconSample() } - item { LargeButtonSample() } - item { LargeButtonWithLeadingIconSample() } - item { LargeButtonWithTrailingIconSample() } - item { LargeButtonWithLeadingAndTrailingIconSample() } - } -} - -@Sampled -@Composable -fun ButtonSample() { - Button(onClick = {}) { Text("Send") } -} - -@Sampled -@Composable -fun ButtonWithLeadingIconSample() { - Button(onClick = {}, leadingIcon = { Icon(FavoriteIcon, "Localized description") }) { - Text("Send") - } -} - -@Composable -private fun ButtonWithTrailingIconSample() { - Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { - Text("Send") - } -} - -@Composable -private fun ButtonWithLeadingAndTrailingIconSample() { - Button( - onClick = {}, - leadingIcon = { Icon(FavoriteIcon, "Localized description") }, - trailingIcon = { Icon(FavoriteIcon, "Localized description") }, - ) { - Text("Send") - } -} - -@Sampled -@Composable -fun LargeButtonSample() { - Button(onClick = {}, buttonSize = ButtonSize.Large) { Text("Send") } -} - -@Composable -private fun LargeButtonWithLeadingIconSample() { - Button( - onClick = {}, - buttonSize = ButtonSize.Large, - leadingIcon = { Icon(FavoriteIcon, "Localized description") }, - ) { - Text("Send") - } -} - -@Composable -private fun LargeButtonWithTrailingIconSample() { - Button( - onClick = {}, - buttonSize = ButtonSize.Large, - trailingIcon = { Icon(FavoriteIcon, "Localized description") }, - ) { - Text("Send") - } -} - -@Composable -private fun LargeButtonWithLeadingAndTrailingIconSample() { - Button( - onClick = {}, - buttonSize = ButtonSize.Large, - leadingIcon = { Icon(FavoriteIcon, "Localized description") }, - trailingIcon = { Icon(FavoriteIcon, "Localized description") }, - ) { - Text("Send") - } -} - -@Preview -@Composable -private fun ButtonPreview() { - GlimmerTheme { ButtonSample() } -} - -@Preview -@Composable -private fun ButtonWithLeadingIconPreview() { - GlimmerTheme { ButtonWithLeadingIconSample() } -} - -@Preview -@Composable -private fun ButtonWithTrailingIconPreview() { - GlimmerTheme { ButtonWithTrailingIconSample() } -} - -@Preview -@Composable -private fun ButtonWithLeadingAndTrailingIconPreview() { - GlimmerTheme { ButtonWithLeadingAndTrailingIconSample() } -} - -@Preview -@Composable -private fun LargeButtonPreview() { - GlimmerTheme { LargeButtonSample() } -} - -@Preview -@Composable -private fun LargeButtonWithLeadingIconPreview() { - GlimmerTheme { LargeButtonWithLeadingIconSample() } -} - -@Preview -@Composable -private fun LargeButtonWithTrailingIconPreview() { - GlimmerTheme { LargeButtonWithTrailingIconSample() } -} - -@Preview -@Composable -private fun LargeButtonWithLeadingAndTrailingIconPreview() { - GlimmerTheme { LargeButtonWithLeadingAndTrailingIconSample() } -} -``` - -
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md deleted file mode 100644 index 134df7c..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md +++ /dev/null @@ -1,420 +0,0 @@ -When creating a Glimmer List component, refer to the following source code in -`ListState.kt` for creating a state for the list: - - -```kotlin -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.xr.glimmer.list - -import androidx.annotation.IntRange -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.ScrollIndicatorState -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.ScrollScope -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList -import androidx.compose.runtime.Composable -import androidx.compose.runtime.annotation.FrequentlyChangingValue -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.neverEqualPolicy -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.layout.AlignmentLine -import androidx.compose.ui.layout.MeasureResult -import androidx.compose.ui.layout.Remeasurement -import androidx.compose.ui.layout.RemeasurementModifier -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Density -import androidx.xr.glimmer.list.ListState.Companion.Saver -import kotlin.math.abs - -/** - * Creates a [ListState] that is remembered across compositions. - * - * Changes to the provided initial values will **not** result in the state being recreated or - * changed in any way if it has already been created. - * - * @param initialFirstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] - * @param initialFirstVisibleItemScrollOffset the initial value for - * [ListState.firstVisibleItemScrollOffset] - */ -@Composable -public fun rememberListState( - initialFirstVisibleItemIndex: Int = 0, - initialFirstVisibleItemScrollOffset: Int = 0, -): ListState = - rememberSaveable(saver = ListState.Saver) { - ListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) - } - -/** - * A state object that can be hoisted to control and observe scrolling. - * - * In most cases, this will be created via [rememberListState]. - * - * @param firstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] - * @param firstVisibleItemScrollOffset the initial value for - * [ListState.firstVisibleItemScrollOffset] - */ -public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) : - ScrollableState { - - private val backingState = ScrollableState { -onScroll(-it) } - - // TODO: b/414961654 - Consider making this abstraction around "anchor item". - /** The holder class for the current scroll position. */ - private val scrollPosition = - GlimmerListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) - - /** Backing state for [layoutInfo] */ - internal val layoutInfoState = mutableStateOf(EmptyLazyListMeasureResult, neverEqualPolicy()) - - private val density: Density - get() = layoutInfoState.value.density - - /** - * This field is used to save information about the number of "beyond bounds items" that we want - * to compose. These items are not within the visible bounds of the lazy layout, but we compose - * them because they are explicitly requested through the - * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout]. - */ - internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() - - /** Includes information for requesting focus for children as the list scrolls. */ - internal val autoFocusState = GlimmerListAutoFocusState() - - /** Stores currently pinned items which are always composed. */ - internal val pinnedItems = LazyLayoutPinnedItemList() - - internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource() - - /** - * The scroll amount was provided in `onScroll` to be consumed during the current measure pass. - * - * Scrolling forward is negative. - */ - internal var incomingScroll: Float = 0f - private set - - /** - * This value retains the scroll amount that we want to carry over between measure passes. It - * represents the amount of scroll from previous passes that wasn't consumed back then but is - * awaiting to be consumed later. Together with [incomingScroll], this represents the total - * scroll available to the list during the current measure pass. - * - * This variable exists because the layout uses integer pixels while [onScroll] operates with - * fractional floats. Consequently, the list might receive tiny fractions of scroll input which, - * when aggregated, should equate to a few pixels of scrolling. However, because these small - * fractions are spread across many [onScroll] invocations, the list ignores them, making it - * feel unresponsive during slow, gentle touches. To avoid this, instead of ignoring these tiny - * scroll increments, the list accumulates them so they can be added to the next scroll part and - * actually consumed in a subsequent pass. - * - * Note that this value can sometimes be opposite to the scroll direction. This occurs because - * the [Float] -> [Int] rounding causes us to consume more than was provided. To compensate, we - * add a negative value to [incomingScroll] the next time. - * - * Scrolling forward is negative. - */ - internal var carryOverScroll: Float = 0f - private set - - /** - * This value is updated after the measure pass inside [applyMeasureResult] and defines how much - * of the [incomingScroll] was actually used. - */ - private var consumedScroll: Float = 0f - - internal val nearestRange: kotlin.ranges.IntRange by - LazyLayoutNearestRangeState(0, NearestItemsSlidingWindowSize, NearestItemsExtraItemCount) - - /** - * The [Remeasurement] object associated with our layout. It allows us to remeasure - * synchronously during scroll. - */ - internal var remeasurement: Remeasurement? = null - private set - - /** The modifier which provides [remeasurement]. */ - internal val remeasurementModifier = - object : RemeasurementModifier { - override fun onRemeasurementAvailable(remeasurement: Remeasurement) { - this@ListState.remeasurement = remeasurement - } - } - - /** - * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is - * ready. - */ - internal val awaitLayoutModifier = AwaitFirstLayoutModifier() - - /** - * The index of the first item that is visible within the scrollable viewport area not including - * items in the content padding region. For the first visible item that includes items in the - * content padding please use [ListLayoutInfo.visibleItemsInfo]. - * - * Note that this property is observable and if you use it in the composable function it will be - * recomposed on every change causing potential performance issues. - */ - public val firstVisibleItemIndex: Int - @FrequentlyChangingValue get() = scrollPosition.index - - /** - * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount - * that the item is offset backwards. - * - * Note that this property is observable and if you use it in the composable function it will be - * recomposed on every scroll causing potential performance issues. - */ - public val firstVisibleItemScrollOffset: Int - @FrequentlyChangingValue get() = scrollPosition.scrollOffset - - /** - * The object of [ListLayoutInfo] calculated during the last layout pass. For example, you can - * use it to calculate what items are currently visible. - * - * Note that this property is observable and is updated after every scroll or remeasure. If you - * use it in the composable function it will be recomposed on every change causing potential - * performance issues including infinity recomposition loop. Therefore, avoid using it in the - * composition. - * - * If you want to run some side effects like sending an analytics event or updating a state - * based on this value consider using "snapshotFlow": - */ - public val layoutInfo: ListLayoutInfo - @FrequentlyChangingValue get() = layoutInfoState.value - - /** - * [InteractionSource] that will be used to dispatch drag events when this list is being - * dragged. If you want to know whether the fling (or animated scroll) is in progress, use - * [isScrollInProgress]. - */ - public val interactionSource: InteractionSource - get() = internalInteractionSource - - /** Snaps to the requested scroll position. */ - internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) { - scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) - remeasurement?.forceRemeasure() - } - - /** - * When the user provided custom keys for the items we can try to detect when there were items - * added or removed before our current first visible item and keep this item as the first - * visible one even given that its index has been changed. - */ - internal fun updateScrollPositionIfTheFirstItemWasMoved( - itemProvider: GlimmerListItemProvider, - firstItemIndex: Int, - ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) - - /** - * Called during the measurement pass once the dispatched [incomingScroll] has been handled and - * the new item positions are known. - * - * @param result lazy list measuring results. - * @param consumedScroll defines how much scroll was consumed during the measure pass. - * @param scrollToCarryOver tracks the amount of scrolling that the internal logic has reported - * as consumed, but wants to save for later measurement. Also, if the list consumes more - * scroll than it was given (for example, due to rounding errors), this value can be set to a - * negative amount to balance it out in the next pass. This value should never be larger than - * [consumedScroll]. - */ - internal fun applyMeasureResult( - result: GlimmerListMeasureResult, - consumedScroll: Float, - scrollToCarryOver: Float, - ) { - this.consumedScroll = consumedScroll - this.carryOverScroll = scrollToCarryOver - - canScrollBackward = result.canScrollBackward - canScrollForward = result.canScrollForward - layoutInfoState.value = result - - scrollPosition.updateFromMeasureResult(result) - } - - internal fun onScroll(distance: Float): Float { - if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { - return 0f - } - - // Fast path. Skip measure pass. - if (abs(distance + carryOverScroll) <= 0.5f) { - // Inside measuring we do `scrollToBeConsumed.roundToInt()` so there will be no scroll - // if we have less than 0.5 pixels. So just accumulate it for the next pass. - carryOverScroll += distance - return distance - } - - incomingScroll = distance - // The `forceRemeasure()` invocation triggers the measure pass where [incomingScroll] - // will be used to update [consumedScroll] and [carryOverScroll] values. - remeasurement?.forceRemeasure() - // It's important to reset this value because there are measure passes - // triggered from outside scrolling. They read this value as well. - // So, after we used it, we need to reset it to zero. - incomingScroll = 0f - - return consumedScroll - } - - override suspend fun scroll( - scrollPriority: MutatePriority, - block: suspend ScrollScope.() -> Unit, - ) { - awaitLayoutModifier.waitForFirstLayout() - backingState.scroll(scrollPriority, block) - } - - override fun dispatchRawDelta(delta: Float): Float = backingState.dispatchRawDelta(delta) - - override val isScrollInProgress: Boolean - get() = backingState.isScrollInProgress - - @get:Suppress("GetterSetterNames") - override var canScrollForward: Boolean by mutableStateOf(false) - private set - - @get:Suppress("GetterSetterNames") - override var canScrollBackward: Boolean by mutableStateOf(false) - private set - - override val scrollIndicatorState: ScrollIndicatorState - get() = _scrollIndicatorState - - private val _scrollIndicatorState = - object : ScrollIndicatorState { - override val scrollOffset: Int - @FrequentlyChangingValue - get() = - with(layoutInfoState.value) { - if (this === EmptyLazyListMeasureResult) { - Int.MAX_VALUE - } else { - this@ListState.firstVisibleItemIndex * visibleItemsAverageSize + - this@ListState.firstVisibleItemScrollOffset - } - } - - override val contentSize: Int - @FrequentlyChangingValue - get() = - with(layoutInfoState.value) { - if (this === EmptyLazyListMeasureResult) { - Int.MAX_VALUE - } else { - // Approximate size of all content - (totalItemsCount * visibleItemsAverageSize) - - // Subtract the final trailing spacing that is not shown - (if (totalItemsCount > 0) mainAxisItemSpacing else 0) + - // Add the inner paddings (which are separate from the item sizes) - beforeContentPadding + - afterContentPadding - } - } - - override val viewportSize: Int - get() = - with(layoutInfoState.value) { - if (this === EmptyLazyListMeasureResult) { - Int.MAX_VALUE - } else { - mainAxisViewportSize - } - } - } - - /** - * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset] - * pixels. - * - * @param index the index to which to scroll. Must be non-negative. - * @param scrollOffset the offset that the item should end up after the scroll. Note that - * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will - * scroll the item further upward (taking it partly offscreen). - */ - public suspend fun scrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { - scroll { snapToItemIndexInternal(index, scrollOffset) } - } - - /** - * Animate (smooth scroll) to the given item. - * - * @param index the index to which to scroll. Must be non-negative. - * @param scrollOffset the offset that the item should end up after the scroll. Note that - * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will - * scroll the item further upward (taking it partly offscreen). - */ - public suspend fun animateScrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { - scroll { - GlimmerListScrollScope(this@ListState, this) - .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density) - } - } - - public companion object { - /** The default [Saver] implementation for [ListState]. */ - public val Saver: Saver = - listSaver( - save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, - restore = { - ListState(firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1]) - }, - ) - } -} - -private val EmptyLazyListMeasureResult = - GlimmerListMeasureResult( - firstVisibleItem = null, - firstVisibleItemScrollOffset = 0, - canScrollForward = false, - consumedScroll = 0f, - measureResult = - object : MeasureResult { - override val width: Int = 0 - override val height: Int = 0 - - @Suppress("PrimitiveInCollection") - override val alignmentLines: Map = emptyMap() - - override fun placeChildren() {} - }, - visibleItemsInfo = emptyList(), - viewportStartOffset = 0, - viewportEndOffset = 0, - totalItemsCount = 0, - reverseLayout = false, - orientation = Orientation.Vertical, - afterContentPadding = 0, - mainAxisItemSpacing = 0, - remeasureNeeded = false, - density = Density(1f), - childConstraints = Constraints(), - ) -``` - -
\ No newline at end of file From 79bee216c47432cbdb576a9bf1a62b65bee56f8f Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Mon, 6 Jul 2026 08:06:29 +0000 Subject: [PATCH 29/61] Updates skills (2026-07-06 08:06) --- build/agp/agp-9-upgrade/SKILL.md | 4 +- .../build/migrate-to-built-in-kotlin.md | 416 ++++++++++ camera/camera1-to-camerax/SKILL.md | 270 ------- camera/camerax/SKILL.md | 125 +++ .../camerax/references/expert-blueprints.md | 58 ++ camera/camerax/references/foldables.md | 83 ++ camera/camerax/references/immutability.md | 60 ++ camera/camerax/references/low-light.md | 124 +++ camera/camerax/references/mlkit-spatial.md | 75 ++ camera/camerax/references/modern-apis.md | 42 + camera/camerax/references/testing.md | 69 ++ camera/camerax/references/thermals.md | 85 ++ camera/camerax/references/wear-os.md | 76 ++ camera/camerax/references/xr.md | 54 ++ identity/verified-email/SKILL.md | 27 +- .../email-verification-implementation.md | 25 +- jetpack-compose/adaptive/SKILL.md | 2 +- .../layouts/adaptive/flexbox/get-started.md | 2 +- .../adaptive/grid/container-properties.md | 81 +- .../layouts/adaptive/grid/get-started.md | 2 +- .../develop/ui/compose/tooling/debug.md | 2 +- .../SKILL.md | 4 +- .../analysis-of-the-project-and-layout.md | 42 + jetpack-compose/theming/styles/SKILL.md | 12 +- navigation/navigation-3/SKILL.md | 8 +- .../navigation-3/recipes/basicdsl.md | 85 ++ .../navigation-3/recipes/deeplinks-basic.md | 744 ++++++++++++++++++ .../navigation-3/recipes/modular-koin.md | 287 +++++++ play/engage-sdk-integration/SKILL.md | 4 +- .../playcore/engage/tv/getting-started.md | 5 +- .../references/patterns.md | 41 +- .../references/requests.md | 231 ++++++ .../SKILL.md | 2 +- security/android-intent-security/SKILL.md | 9 +- testing/testing-setup/SKILL.md | 2 +- .../ui/compose/testing/common-patterns.md | 2 +- wear/wear-compose-m3/SKILL.md | 2 +- .../wearables/compose/migrate-to-material3.md | 10 +- .../SKILL.md | 6 +- .../references/button-samples-source.md | 176 +++++ .../references/card-samples-source.md | 21 +- .../references/card-source.md | 26 +- .../references/liststate-source.md | 420 ++++++++++ 43 files changed, 3457 insertions(+), 364 deletions(-) create mode 100644 build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md delete mode 100644 camera/camera1-to-camerax/SKILL.md create mode 100644 camera/camerax/SKILL.md create mode 100644 camera/camerax/references/expert-blueprints.md create mode 100644 camera/camerax/references/foldables.md create mode 100644 camera/camerax/references/immutability.md create mode 100644 camera/camerax/references/low-light.md create mode 100644 camera/camerax/references/mlkit-spatial.md create mode 100644 camera/camerax/references/modern-apis.md create mode 100644 camera/camerax/references/testing.md create mode 100644 camera/camerax/references/thermals.md create mode 100644 camera/camerax/references/wear-os.md create mode 100644 camera/camerax/references/xr.md create mode 100644 jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md create mode 100644 play/engage-sdk-integration/references/requests.md create mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md create mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build/agp/agp-9-upgrade/SKILL.md index 1ffa0db..280347a 100644 --- a/build/agp/agp-9-upgrade/SKILL.md +++ b/build/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-06-03' + last-updated: '2026-06-25' keywords: - Android Gradle Plugin 9 - AGP 9 @@ -51,7 +51,7 @@ If Hilt is used in the project, ensure it is on version 2.59.2 or higher. ### Step 2: Migrate to built-in Kotlin -See [the guide](https://developer.android.com/build/migrate-to-built-in-kotlin) for detailed information. +See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for detailed information. ### Step 3. Migrate to the new AGP DSL diff --git a/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md b/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md new file mode 100644 index 0000000..7306e7b --- /dev/null +++ b/build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md @@ -0,0 +1,416 @@ +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`. + +> [!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. + +## 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: + + Failed to apply plugin 'org.jetbrains.kotlin.android'. + > Cannot add extension with name 'kotlin', as there is an extension already registered with that name. + +...or + + 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: + +> [!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). + +1. [Remove the `kotlin-android` plugin](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-remove-kotlin-android-plugin) +2. [Migrate the `kotlin-kapt` plugin if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-kapt-plugin) +3. [Migrate the `android.kotlinOptions{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-options) +4. [Migrate the `kotlin.sourceSets{}` DSL if necessary](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps-migrate-kotlin-source-sets) + +### 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. + +### With version catalogs + +### Kotlin + +```kotlin +// Module-level build file +plugins { + alias(libs.plugins.kotlin.android) +} +``` + +### Groovy + +```groovy +// Module-level build file +plugins { + alias(libs.plugins.kotlin.android) +} +``` + +### No version catalogs + +### Kotlin + +```kotlin +// Module-level build file +plugins { + id("org.jetbrains.kotlin.android") +} +``` + +### Groovy + +```groovy +// Module-level build file +plugins { + id 'org.jetbrains.kotlin.android' +} +``` + +Then, remove the plugin from your top-level build file: + +### With version catalogs + +### Kotlin + +```kotlin +// Top-level build file +plugins { + alias(libs.plugins.kotlin.android) apply false +} +``` + +### Groovy + +```groovy +// Top-level build file +plugins { + alias(libs.plugins.kotlin.android) apply false +} +``` + +### No version catalogs + +### Kotlin + +```kotlin +// Top-level build file +plugins { + id("org.jetbrains.kotlin.android") version "KOTLIN_VERSION" apply false +} +``` + +### Groovy + +```groovy +// Top-level build file +plugins { + id 'org.jetbrains.kotlin.android' version 'KOTLIN_VERSION' apply false +} +``` + +If you use version catalogs, also remove the plugin definition from the +version catalog TOML file (usually `gradle/libs.versions.toml`): + +```toml +[plugins] +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "KOTLIN_VERSION" } +``` + +### 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). + +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: + +```toml +[plugins] +android-application = { id = "com.android.application", version.ref = "AGP_VERSION" } + +# Add the following plugin definition +legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "AGP_VERSION" } + +# Remove the following plugin definition +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "KOTLIN_VERSION" } +``` + +Then, update your build files: + +### Kotlin + +```kotlin +// Top-level build file +plugins { + alias(libs.plugins.legacy.kapt) apply false + alias(libs.plugins.kotlin.kapt) apply false +} +``` + +### Groovy + +```groovy +// Top-level build file +plugins { + alias(libs.plugins.legacy.kapt) apply false + alias(libs.plugins.kotlin.kapt) apply false +} +``` + +### Kotlin + +```kotlin +// Module-level build file +plugins { + alias(libs.plugins.legacy.kapt) + alias(libs.plugins.kotlin.kapt) +} +``` + +### Groovy + +```groovy +// Module-level build file +plugins { + alias(libs.plugins.legacy.kapt) + alias(libs.plugins.kotlin.kapt) +} +``` + +> [!NOTE] +> **Note:** If you declare the `kotlin-kapt` plugin in the `plugins{}` block as `kotlin("kapt") version ""`, then remove that line instead. + +### 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. + +For example, update this code: + +### Kotlin + +```kotlin +android { + kotlinOptions { + languageVersion = "2.0" + jvmTarget = "11" + } +} +``` + +### Groovy + +```groovy +android { + kotlinOptions { + languageVersion = "2.0" + jvmTarget = "11" + } +} +``` + +...to the new DSL: + +### Kotlin + +```kotlin +kotlin { + compilerOptions { + languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0 + // Optional: Set jvmTarget + // jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} +``` + +### Groovy + +```groovy +kotlin { + compilerOptions { + languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0 + // Optional: Set jvmTarget + // jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} +``` + +> [!NOTE] +> **Note:** With built-in Kotlin, you don't need to set `kotlin.compilerOptions.jvmTarget` because its value defaults to `android.compileOptions.targetCompatibility`. + +### 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. + +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 + +```kotlin +# Adding Kotlin source directories to kotlin.sourceSets is not supported +kotlin.sourceSets.named("main") { + kotlin.srcDir("additionalSourceDirectory/kotlin") +} + +# Adding Kotlin source directories to AndroidSourceSet.java is also not supported +android.sourceSets.named("main") { + java.directories += "additionalSourceDirectory/kotlin" +} + +# Add Kotlin source directories to AndroidSourceSet.kotlin +android.sourceSets.named("main") { + kotlin.directories += "additionalSourceDirectory/kotlin" +} +``` + +### Groovy + +```groovy +# Adding Kotlin source directories to kotlin.sourceSets is not supported +kotlin.sourceSets.named("main") { + kotlin.srcDir("additionalSourceDirectory/kotlin") +} + +# Adding Kotlin source directories to AndroidSourceSet.java is also not supported +android.sourceSets.named("main") { + java.directories.add("additionalSourceDirectory/kotlin") +} + +# Add Kotlin source directories to AndroidSourceSet.kotlin +android.sourceSets.named("main") { + kotlin.directories.add("additionalSourceDirectory/kotlin") +} +``` + +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 + +```kotlin +androidComponents.onVariants { variant -> + variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin") + variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT) +} +``` + +### Groovy + +```groovy +androidComponents.onVariants { variant -> + variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin") + variant.sources.kotlin!!.addGeneratedSourceDirectory(TASK_PROVIDER, TASK_OUTPUT) +} +``` + +## 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. + +## 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. + +> [!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). + +## 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. + +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. + +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. + +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. + +To disable built-in Kotlin for a module, +set `enableKotlin = false` in that module's build file: + +### Kotlin + +```kotlin +android { + enableKotlin = false +} +``` + +### Groovy + +```groovy +android { + enableKotlin = false +} +``` \ No newline at end of file diff --git a/camera/camera1-to-camerax/SKILL.md b/camera/camera1-to-camerax/SKILL.md deleted file mode 100644 index b47f97c..0000000 --- a/camera/camera1-to-camerax/SKILL.md +++ /dev/null @@ -1,270 +0,0 @@ ---- -name: camera1-to-camerax -description: Use this skill to migrate legacy Android camera implementations (Camera1 - or raw Camera2 APIs) to CameraX. CameraX is a lifecycle-aware Jetpack library built - on top of Camera2 that resolves camera rotation issues and handles device dependencies. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-05-06' - keywords: - - Android - - CameraX - - Camera1 Migration - - Jetpack Compose - - Dependencies - - Image Capture - - Lifecycle - - PreviewView ---- - -## Step 0: Add Dependencies - -Check for and add the required CameraX dependencies. Use version 1.3.0 or higher -for interoperability, or version 1.5.0 or higher for Compose extensions. - -If you are using a Version Catalog (`libs.versions.toml`), add the following: - - -```kotlin -[versions] -camerax = "" - -[libraries] -androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } -androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } -androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } -androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } -androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" } -``` - -
- -And in your `build.gradle.kts` (or `build.gradle`): - - -```kotlin -implementation(libs.androidx.camera.core) -implementation(libs.androidx.camera.camera2) -implementation(libs.androidx.camera.lifecycle) -implementation(libs.androidx.camera.view) -implementation(libs.androidx.camera.compose) -``` - -
- -Without a Version Catalog, fall back to these standard Gradle dependencies: - - -```kotlin -implementation "androidx.camera:camera-core:" -implementation "androidx.camera:camera-camera2:" -implementation "androidx.camera:camera-lifecycle:" -implementation "androidx.camera:camera-view:" -implementation "androidx.camera:camera-compose:" -``` - -
- -## Step 1: Remove Legacy Implementation - -1. Delete all `android.hardware.Camera` instances. -2. Delete `SurfaceView` and `SurfaceHolder.Callback` implementations (`surfaceCreated`, `surfaceChanged`, `surfaceDestroyed`). -3. Remove custom lifecycle handling that opens or releases the camera in `onResume` or `onPause`. -4. Remove manual matrix calculations for orientation. - -## Step 2: Initialize ProcessCameraProvider - -Request the `ProcessCameraProvider` and bind use cases to the Activity or -Fragment lifecycle. - - -```kotlin -val context = LocalContext.current -val lifecycleOwner = LocalLifecycleOwner.current -LaunchedEffect(context, lifecycleOwner) { - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - cameraProviderFuture.addListener({ - val cameraProvider = cameraProviderFuture.get() - - val cameraSelector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - - val preview = Preview.Builder().build() - val imageCapture = ImageCapture.Builder() - .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) - .build() - - cameraProvider.unbindAll() // Unbind before rebinding - - val camera = cameraProvider.bindToLifecycle( - lifecycleOwner, - cameraSelector, - preview, - imageCapture - ) - val cameraControl = camera.cameraControl - }, ContextCompat.getMainExecutor(context) - ) -} -``` - -
- -## Step 3: Implement the Preview \& Tap-to-Focus - -Choose exactly one of the following patterns based on the app's UI toolkit: - -### Option A: For Android Views (XML Legacy) - -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) -``` - -
- -### Option B: For Jetpack Compose - -Use `androidx.camera.compose.CameraXViewfinder`. - -**1. Set up preview and SurfaceRequest**: - - -```kotlin -var surfaceRequest by remember { mutableStateOf(null) } -val preview = remember { - Preview.Builder().build().apply { - setSurfaceProvider { request -> surfaceRequest = request } - } -} -``` - -
- -**2. Render viewfinder**: - - -```kotlin -surfaceRequest?.let { request -> - CameraXViewfinder( - surfaceRequest = request, - coordinateTransformer = coordinateTransformer, - modifier = Modifier - ) -} -``` - -
- -**3. Handle tap-to-focus in Compose**: - - -```kotlin -// Inside your tap gesture handler... -val surfaceCoords = with(coordinateTransformer) { offset.transform() } -val factory = SurfaceOrientedMeteringPointFactory( - request.resolution.width.toFloat(), - request.resolution.height.toFloat() -) -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) { - if (!view.isInEditMode) { - val rotation = view.display?.rotation ?: Surface.ROTATION_0 - imageCapture.targetRotation = rotation - preview.targetRotation = rotation - } -} -``` - -
- -## Step 4: Capture Photo - -Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles -rotation directly. - - -```kotlin -imageCapture.takePicture( - cameraExecutor, - object : ImageCapture.OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { - val buffer = image.planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) - val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - - // Adjust rotation natively via ImageProxy - val matrix = Matrix() - matrix.postRotate(image.imageInfo.rotationDegrees.toFloat()) - if (lensFacing == CameraSelector.LENS_FACING_FRONT) { - matrix.postScale(-1f, 1f) // Mirror for front camera - } - - val rotatedBitmap = Bitmap.createBitmap( - bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true - ) - - // MUST close proxy - image.close() - } - - override fun onError(exception: ImageCaptureException) { - Log.e("CameraX", "Capture failed: ${exception.message}", exception) - } - } -) -``` - -
- -## Step 5: Switch Cameras - -To flip between front and rear cameras, change the `CameraSelector` and -re-trigger the `ProcessCameraProvider` logic. - - -```kotlin -lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) { - CameraSelector.LENS_FACING_FRONT -} else { - CameraSelector.LENS_FACING_BACK -} -``` - -
- -## Constraints - -- **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`. -- **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations. -- **Don't forget to close the ImageProxy** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos. -- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an old fallback option that introduces resizing issues. diff --git a/camera/camerax/SKILL.md b/camera/camerax/SKILL.md new file mode 100644 index 0000000..e6eebe1 --- /dev/null +++ b/camera/camerax/SKILL.md @@ -0,0 +1,125 @@ +--- +name: camerax +description: Provide technical guidance for Android camera development with CameraX. + Use when implementing camera features, handling asynchronous recording lifecycles, + wiring low-level hardware interop using CameraX, or integrating ML Kit or Media3 + effects. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-02' + keywords: + - recipe + - Android + - Camera + - Camera1 + - Camera2 + - CameraX + - migration + - Compose + - guide + - dependencies + - PreviewView + - CameraXViewfinder + - ImageCapture + - VideoCapture + - 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. + +## 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. + +**Pattern: Reassignment is required** + + +```kotlin +// WRONG +run { + val pending = recorder.prepareRecording(context, opts) + pending.withAudioEnabled() // This returns a new instance which is ignored + val active = pending.start(exec, listener) +} + +// CORRECT +run { + val pending = recorder.prepareRecording(context, opts) + .withAudioEnabled() // Chaining works + val active = pending.start(exec, listener) +} + +// ALSO CORRECT +run { + var pending = recorder.prepareRecording(context, opts) + pending = pending.withAudioEnabled() // Reassignment + val active = pending.start(exec, listener) +} +``` + +
+ +See [immutability](references/immutability.md) for a list of affected classes. + +### 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: + +- **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. +- **Custom effects** : Prefer `Media3Effect` or `SurfaceProcessor` over manual OpenGL pipelines unless absolute performance is required. +- **Low-light** : See [low-light](references/low-light.md) for Night Mode and LLB guidance. +- **Foldables** : See [foldables](references/foldables.md) for handling dynamic postures and hinge states. +- **XR, AR, and VR** : See [xr](references/xr.md) for spatial tracking, passthrough synchronization, and latency guardrails. +- **Thermals and power** : See [thermals](references/thermals.md) for managing `StreamUseCase` optimizations and `PowerManager` thermal states. +- **Testing and mocking** : See [testing](references/testing.md) for using `FakeCameraConfig`, handling asynchronous lifecycles, and validating analysis pipelines. +- **ML Kit spatial analysis** : See [mlkit-spatial](references/mlkit-spatial.md) for coordinate mapping, rotation logic, and mirrored lens handling. +- **Wear OS camera remote** : See [wear-os](references/wear-os.md) for circular UI constraints, Data Layer API syncing, and remote trigger logic. + +See [expert-blueprints](references/expert-blueprints.md) for step-by-step guides. + +### API discovery + +Always use higher-level abstractions instead of low-level manual wiring: + +- **Analysis** : Use `MlKitAnalyzer` instead of manual `ImageAnalysis.Analyzer`. +- **Filters and effects** : Use `Media3Effect` for standard post-processing. +- **Multi-camera** : Use `ConcurrentCamera` APIs for dual-stream setups. + +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: + +- **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. + +## 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. + +- **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. +- **Feature parity**: Features like flash or auto-focus behave differently across hardware. For example, CameraX handles both physical flash, back, and screen-based flash, front, and both must be considered when implementing flash functionality. + +## Common pitfalls + +- **Asynchronous lifecycles** : Check `isRecording` state before attempting to stop or pause. Handle `VideoRecordEvent.Start` for UI state updates, not just the initial call. +- **Thread safety**: Camera callbacks often run on background executors. Dispatch UI updates on the main thread. +- **Permission handling** : Check `CAMERA` permission; check for `RECORD_AUDIO` specifically when enabling audio in `VideoCapture`. diff --git a/camera/camerax/references/expert-blueprints.md b/camera/camerax/references/expert-blueprints.md new file mode 100644 index 0000000..e1f2f25 --- /dev/null +++ b/camera/camerax/references/expert-blueprints.md @@ -0,0 +1,58 @@ +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 + +### Phase one: ViewModel and state + +1. Define a `ManualSettings` data class. +2. Add a `MutableStateFlow` to your `CameraViewModel`. +3. Implement the Jetpack Compose UI with sliders and switches to update this flow. + +### Phase two: Controller wiring + +1. In `CameraController.kt`, create a new function `updateManualSettings(settings: ManualSettings)`. +2. Map these settings into the `CameraSystem` layer. + +### Phase three: CameraX `Camera2Interop` wiring + +1. In `CameraSession.kt`, use the `Camera2Interop.Extender` utility to access Camera2 capture request keys. +2. Apply the hardware keys: + - `CaptureRequest.SENSOR_SENSITIVITY` to set ISO sensitivity. + - `CaptureRequest.SENSOR_EXPOSURE_TIME` to set exposure time. + - `CaptureRequest.LENS_FOCUS_DISTANCE` to set focus distance. +3. **Critical** : If manual exposure is active, set `CaptureRequest.CONTROL_AE_MODE` to `CameraMetadata.CONTROL_AE_MODE_OFF`. --- + +## RAW and JPEG capture + +### Phase one: Output configuration + +1. Verify device support for RAW capture using `CameraInfo`. +2. Configure `ImageCapture.Builder` with `OUTPUT_FORMAT_RAW_JPEG` or `OUTPUT_FORMAT_RAW`. + +### Phase two: Implementation + +1. Provide `ImageCapture.OutputFileOptions` for the target storage locations. +2. Invoke `takePicture`. CameraX internally manages `DngCreator` to wrap RAW data with the required `CameraCharacteristics` and `CaptureResult` metadata. + +*** ** * ** *** + +## Apply image effects + +### Phase one: Effects selection + +1. Use the `androidx.media3:media3-effect` dependency. +2. Use `RgbFilter` or `HslAdjustment` for standard color grading. + +### Phase two: Application + +1. Configure `Composition.Builder` or `MediaItem.Builder` with the list of effects. +2. Inject the list of effects into the CameraX `Recorder` or `Preview` using the `setEffects` method. + +*** ** * ** *** + +## 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 diff --git a/camera/camerax/references/foldables.md b/camera/camerax/references/foldables.md new file mode 100644 index 0000000..22d378a --- /dev/null +++ b/camera/camerax/references/foldables.md @@ -0,0 +1,83 @@ +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 + +| State | Posture | User interaction | Implementation goal | +|---|---|---|---| +| `FLAT` | Standard | Full screen preview | Conventional mobile phone layout. | +| `HALF_OPENED` | Tabletop | Lower half for controls | Split-screen layout, viewfinder on top, controls on bottom. | +| `HALF_OPENED` | Book | Side-by-side | Viewfinder on one panel, gallery and controls on the other. | + +*** ** * ** *** + +## Follow the implementation guide + +### Detect posture changes + +Use the Jetpack WindowManager library to observe the device's hinge state and +fold layout. + + +```kotlin +lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + windowInfoTracker.windowLayoutInfo(activity) + .collect { layoutInfo -> + val displayFeature = layoutInfo.displayFeatures + .filterIsInstance() + .firstOrNull() + + updateCameraLayout(displayFeature) + } + } +} +``` + +
+ +### 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. + +- **Identify orientation:** Check `FoldingFeature.orientation`. +- **Calculate geometry:** Use `FoldingFeature.bounds` to identify the hinge's physical location on the screen. +- **Update UI:** Apply padding or constraints to move the `PreviewView` above the hinge. + +### 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. + + +```kotlin +val viewport = ViewPort.Builder(Rational(viewfinder.width, viewfinder.height), display.rotation) + .setScaleType(ViewPort.FILL_CENTER) + .build() + +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. + +- **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. + +*** ** * ** *** + +## Foldable pitfalls + +- **Hinge distortion:** Don't span the camera preview across a hinge in `HALF_OPENED` state. +- **Physical orientation:** The camera sensor's physical orientation relative to the screen often changes when you fold or unfold the device. Always rely on `CameraInfo.getSensorRotationDegrees`. +- **Latency:** Rebinding `UseCase` objects during a fold event is expensive. Use the internal scaling of `PreviewView` before performing a full `bindToLifecycle` reconfiguration. \ No newline at end of file diff --git a/camera/camerax/references/immutability.md b/camera/camerax/references/immutability.md new file mode 100644 index 0000000..1dade21 --- /dev/null +++ b/camera/camerax/references/immutability.md @@ -0,0 +1,60 @@ +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. + +| Class | Methods that return a new instance | Result if not reassigned | +|---|---|---| +| `PendingRecording` | `withAudioEnabled`, `asPersistentRecording` | Audio isn't recorded. | +| `ImageCapture.Builder` | `setTargetRotation`, `setTargetResolution` | The output has the wrong orientation. | +| `Recorder.Builder` | `setQualitySelector`, `setExecutor` | The recording uses the default quality. | +| `Viewport.Builder` | `setScaleType`, `setLayoutDirection` | The viewfinder is stretched. | + +## Use standard patterns + +### CameraX video recording + +To set up video recording, use the following code: + + +```kotlin +// WRONG +run { + val pending = recorder.prepareRecording(context, opts) + pending.withAudioEnabled() // This returns a new instance which is ignored + val active = pending.start(exec, listener) +} + +// CORRECT +run { + val pending = recorder.prepareRecording(context, opts) + .withAudioEnabled() // Chaining works + val active = pending.start(exec, listener) +} + +// ALSO CORRECT +run { + var pending = recorder.prepareRecording(context, opts) + pending = pending.withAudioEnabled() // Reassignment + val active = pending.start(exec, listener) +} +``` + +
+ +### Viewport construction + +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 new file mode 100644 index 0000000..e03e00c --- /dev/null +++ b/camera/camerax/references/low-light.md @@ -0,0 +1,124 @@ +This guide covers implementing low-light features using **Night mode +extensions** and **Low Light Boost (LLB)**. + +## Choosing the right tool + +| Feature | Used for | Implementation | UX impact | +|---|---|---|---| +| **Night mode** | High-quality stills | `ExtensionsManager` | Possibly requires user to hold still for several seconds. | +| **LLB, AE mode** | Real-time preview or video | `Camera2Interop`, CameraX utility | Hardware drops the frame rate to increase brightness. | +| **LLB, Play services** | Real-time preview and video | `SurfaceProcessor` | Software-based brightening; maintains a higher frame rate. | + +*** ** * ** *** + +## Night mode extension + +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 +val extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).await() +if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) { + val nightSelector = extensionsManager.getExtensionEnabledCameraSelector( + cameraSelector, ExtensionMode.NIGHT + ) + cameraProvider.bindToLifecycle(lifecycleOwner, nightSelector, imageCapture, preview) +} +``` + +
+ +### Comprehensive features + +- **Image postview** : Display a low-resolution image immediately while the multi-frame processing occurs. + + ```kotlin + val imageCapture = ImageCapture.Builder() + .setPostviewEnabled(true) + .build() + ``` +- **Extension strength** : Let users control the intensity of the night effect. + + ```kotlin + // Set the strength of the active extension (e.g. NIGHT mode intensity) + 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. + + ```kotlin + // Use the suspend extension function for takePicture to avoid callback boilerplate + try { + val result = imageCapture.takePicture(outputOptions) + // Use result.savedUri or other fields + } catch (e: ImageCaptureException) { + // Handle capture failure + } + ``` + +*** ** * ** *** + +## Low-light boost + +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. + +- **Activation** : Use `CameraControl.enableLowLightBoostAsync`. +- **Implementation** : + + ```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. + +### Google Play services LLB + +It's a multi-step implementation that uses a session-based `SurfaceProcessor`. + +**Dependency** : `com.google.android.gms:play-services-camera-low-light-boost` + +To implement Google Play services LLB, follow these core steps: + +1. **Initialize client** : `val client = LowLightBoost.getClient`. +2. **Implement `SurfaceProcessor`** : + - **Manage session** : Call `client.createSession`. + - **Forward required metadata** : Observe the camera's `TotalCaptureResult` stream and forward every result to the session: `session.processCaptureResult`. + - **Provide surface** : Get the input surface from the session, `session.getCameraSurface`, and provide it to the camera's `SurfaceRequest`. + - **Lifecycle** : Release the session, `session.release`, when the processor is closed or the `SurfaceRequest` completes. +3. **Wire using `CameraEffect`** : + + ```kotlin + val effect = SimpleCameraEffect( + CameraEffect.PREVIEW or CameraEffect.VIDEO_CAPTURE, + executor, + llbSurfaceProcessor + ) { throw it } + + // Add to UseCaseGroup + val useCaseGroup = UseCaseGroup.Builder() + .addUseCase(preview) + .addUseCase(videoCapture) + .addEffect(effect) + .build() + ``` +4. **Scene detection** : Use `session.setSceneDetectorCallback` to receive `boostStrength` updates for real-time UI indicators. + +*** ** * ** *** + +## Implementation notes + +- **Thread safety** : Always handle `ExtensionsManager` and `LowLightBoostClient` initialization asynchronously. +- **FPS trade-offs**: AE mode LLB often drops the frame rate significantly to increase brightness. +- **Compatibility** : Extensions, Night Mode, possibly conflict with `ConcurrentCamera`. Always verify support before binding. \ No newline at end of file diff --git a/camera/camerax/references/mlkit-spatial.md b/camera/camerax/references/mlkit-spatial.md new file mode 100644 index 0000000..833daf1 --- /dev/null +++ b/camera/camerax/references/mlkit-spatial.md @@ -0,0 +1,75 @@ +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 + +| Dimension | Analysis frame | Viewfinder UI on the screen | +|---|---|---| +| Resolution | Fixed, such as 640x480 | Dynamic, such as 1080x2400 | +| Rotation | 0° for the raw buffer | 90° or 270° in portrait or landscape mode | +| Origin | Top-left of buffer at (0,0) | Top-left of screen at (0,0) | + +*** ** * ** *** + +## Follow the implementation guide + +### Coordinate transformation matrix + +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. + +To map the coordinates, use the following workflow: + +1. Query `imageProxy.imageInfo.rotationDegrees`. +2. Pass this rotation to the ML Kit `InputImage`. +3. Use the `MappingUtils.transformRect` method to map the result `Rect` to the screen. + +### 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**. + + +```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`. + +### Overlays and canvas clipping + +Use a custom `GraphicOverlay` view on top of the `PreviewView`. + +- **Buffer lock** : Ensure your `GraphicOverlay` clears its canvas every time a new `ImageAnalysis` frame is processed to prevent "ghosting" of bounding boxes. + +*** ** * ** *** + +## Spatial pitfalls + +- **The "stretched box" bug** : Caused by assuming the Analysis Frame aspect ratio, 4:3, matches the screen aspect ratio, 21:9. Use `PreviewView.SCALE_TYPE_FILL_CENTER` and map coordinates accordingly. +- **Latency** : If ML processing exceeds 50 ms, the bounding box trails behind the user's face. + - **Fix** : Use `ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST` to avoid queuing stale frames. +- **Sensor vs. display rotation** : On some tablets, the sensor is mounted horizontally. Always query `display.rotation` and `cameraInfo.sensorRotationDegrees`. \ No newline at end of file diff --git a/camera/camerax/references/modern-apis.md b/camera/camerax/references/modern-apis.md new file mode 100644 index 0000000..616d549 --- /dev/null +++ b/camera/camerax/references/modern-apis.md @@ -0,0 +1,42 @@ +Always prefer these various abstractions over legacy Camera2 or early CameraX +implementations. + +## Compare APIs + +| Use case | Legacy or verbose way | Recommendation | +|---|---|---| +| QR or face scanning | `ImageAnalysis.Analyzer` and manual ByteBuffer math | **`MlKitAnalyzer`**, which automates coordinate mapping and multi-format support | +| Post-processing | Custom OpenGL shaders or `SurfaceTexture` | **`Media3Effect`** for composable, declarative effects | +| Dual camera | Manual binding of two UseCases | **`ConcurrentCamera`**, which provides built-in support in CameraX 1.3 and higher | +| High dynamic range | Manual bit-depth and profile config | **`DynamicRange`** , which uses `DYNAMIC_RANGE_HLG10` or `SDR` | +| Zoom and focus | `Camera2Interop` for CameraX-to-Camera2 mapping | **`CameraControl.setZoomRatio`** or **`setLinearZoom`** | + +## Hardware awareness + +Modern APIs abstract away the complexity of hardware diversity. + +- **`CameraSelector`** : Use `DEFAULT_BACK_CAMERA` or `DEFAULT_FRONT_CAMERA` instead of hardcoding camera IDs. Use `filter` if you need specific lens capabilities. +- **Extensions** : Before enabling advanced modes, such as night, bokeh, and face retouch, use `ExtensionsManager` to query whether the device supports them. +- **Foldables** : Observe `Lifecycle` and `Viewport` updates to handle posture changes, like a half-opened posture, on foldable devices. + +## Required dependencies + +Add the following dependencies to your `libs.versions.toml` file: + + # CameraX ML Kit + + androidx-camera-mlkit-vision = { group = "androidx.camera", name = + "camera-mlkit-vision", version.ref = "camerax" } + + # Media3 effects + + androidx-media3-effect = { group = "androidx.media3", name = "media3-effect", + version.ref = "media3" } + + # Camera extensions + + 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 diff --git a/camera/camerax/references/testing.md b/camera/camerax/references/testing.md new file mode 100644 index 0000000..972f285 --- /dev/null +++ b/camera/camerax/references/testing.md @@ -0,0 +1,69 @@ +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 + +| Challenge | Conventional approach | Camera technical approach | +|---|---|---| +| **Frameworks** | `Mockito` or `MockK` | **Fakes over mocks** | +| **Assertions** | `assertEquals`, `assertTrue` | **Google Truth, `assertThat`** | +| **Environment** | Implied environments | **Explicit `@RunWith` annotations** | +| **Async operations** | `Thread.sleep` | **Explicit `timeoutMillis` or `IdlingResource`** | + +*** ** * ** *** + +## Follow the implementation guide + +### 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. + + +```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. + + +```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. + +### Test asynchronous lifecycles + +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: + +1. Wrap the `ProcessCameraProvider` initialization in a `CountDownLatch` or `IdlingResource`. +2. Assert only after the `cameraControl` instance is non-null. + +*** ** * ** *** + +## Testing pitfalls + +- **Resource leaks** : To prevent "Camera in Use" errors, in your `@After` block, call `cameraProvider.unbindAll`. +- **The "flaky initializer"**: Camera tests often fail on CI because the "Virtual Camera" takes too long to warm up. Use a sufficient explicit timeout for the first initialization. +- **Permission blockers** : To bypass the system permission dialogs, in your Espresso tests, use `GrantPermissionRule`. +- **Resolution mismatch** : Tests on emulators often default to 640x480. Ensure your `ResolutionSelector` handles this low-res fallback correctly. \ No newline at end of file diff --git a/camera/camerax/references/thermals.md b/camera/camerax/references/thermals.md new file mode 100644 index 0000000..41cfe19 --- /dev/null +++ b/camera/camerax/references/thermals.md @@ -0,0 +1,85 @@ +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 + +| Priority | Strategy | Implementation | +|---|---|---| +| **1. Inform** | Use case hints | Provide `StreamUseCase` to allow the OS to optimize hardware. | +| **2. Monitor** | Thermal state listener | Observe `PowerManager.addThermalStatusListener`. | +| **3. Act** | Graceful degradation | Dynamically reduce FPS, resolution, or disable demanding effects such as HDR. | + +*** ** * ** *** + +## Follow the implementation guide + +### 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. + + +```kotlin +// In CameraX: Set the hint on your Use Case +val preview = Preview.Builder() + .setTargetName("Preview") + .apply { + Camera2Interop.Extender(this).setStreamUseCase( + CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong() + ) + } + .build() +``` + +
+ +Review the following key use cases for stream optimization: + +- `PREVIEW`: This option is the default and provides a balanced configuration. +- `STILL_CAPTURE`: This option provides high-quality capture for short bursts. +- `VIDEO_RECORD`: This option maintains sustained power and is optimized for encoding. +- `VIDEO_CALL`: This option minimizes power consumption for long-duration sessions. + +### Monitor thermal status + +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 +powerManager.addThermalStatusListener { status -> + when (status) { + PowerManager.THERMAL_STATUS_MODERATE -> { + // Signal to UI: "Device is warming up" + } + PowerManager.THERMAL_STATUS_SEVERE -> { + // ACTION: Reduce Frame Rate from 60fps to 30fps + // ACTION: Disable HDR or High-Quality Post-processing + } + PowerManager.THERMAL_STATUS_CRITICAL -> { + // ACTION: Close the camera session to prevent hardware damage + } + } +} +``` + +
+ +### Graceful degradation tiers + +| Tier | Action | User impact | +|---|---|---| +| **Mild** | Stop background analysis using ML Kit | Minimal | +| **Moderate** | Cap frame rate to 30 FPS | Noticeable but smooth | +| **Severe** | Drop resolution from 1080p to 720p | Significant visual change | +| **Critical** | Shut down the session | App unusable in safe mode | + +*** ** * ** *** + +## Thermal pitfalls + +- **The "double work" bug**: Don't run two high-resolution streams---such as a preview and a video capture stream---at different aspect ratios unless necessary. This forces the image signal processor (ISP) to perform double the scaling work, which generates excessive heat. +- **Surface overload** : Don't use multi-step `SurfaceProcessor` or `Media3Effect` chains during `THERMAL_STATUS_SEVERE`. +- **Flash heat** : Flash or torch usage generates high thermal load. Proactively disable the flash if thermal status is `SEVERE`. \ No newline at end of file diff --git a/camera/camerax/references/wear-os.md b/camera/camerax/references/wear-os.md new file mode 100644 index 0000000..4a942db --- /dev/null +++ b/camera/camerax/references/wear-os.md @@ -0,0 +1,76 @@ +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 + +| Feature | Phone camera app | Wear OS remote app | +|---|---|---| +| **Screen** | Rectangular (Large) | **Circular and less than 2 inches in size** | +| **Connectivity** | Local Hardware | **Bluetooth or Wi-Fi data layer API** | +| **Latency** | Direct and less than 20 ms | **Networked, 100 ms to 500 ms** | +| **Interaction** | Multi-touch gestures | **Rotary input or single taps** | + +*** ** * ** *** + +## Follow the implementation guide + +### The circular UI challenge + +Wear OS devices are often round. Standard rectangular layouts clip corner +buttons. + +Follow these blueprint recommendations: + +- Use `Horologist` or `Wear Compose` libraries. +- Use `ScalingLazyColumn` for lists so that items stay within the "safe zone" of the circular display. +- **Preview scaling**: Crop the center of the rectangular phone viewfinder to fit the circular watch screen. + +### Stream the viewfinder + +You can't send a raw 60 fps stream over Bluetooth. compress and throttle. + + +```kotlin +// Example: Sending a viewfinder frame to the watch +val bitmap = previewView.bitmap // Capture current frame +if (bitmap != null) { + val compressed = compressToJpeg(bitmap, quality = 50) + val request = PutDataMapRequest.create("/camera/preview").apply { + dataMap.putAsset("image", Asset.createFromBytes(compressed)) + } + Wearable.getDataClient(context).putDataItem(request.asPutDataRequest()) +} +``` + +
+ +**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." + + +```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**. + +*** ** * ** *** + +## Wear OS pitfalls + +- **Corner clipping** : Placing a **Close** button in the top-right corner of a square layout makes it impossible to tap the button on a round watch display. +- **Battery drain**: Sustained Bluetooth data transfer, viewfinder sync, drains watch battery. Proactively close the remote app if the phone screen is turned off. +- **Node discovery** : The phone is possibly connected to multiple "Nodes" (watches, earbuds, or tablets). Ensure your `CapabilityClient` filters for the specific `camera_remote_host` capability. +- **Disconnect handling**: If the watch disconnects, the phone camera must stop its high-power preview to save energy. \ No newline at end of file diff --git a/camera/camerax/references/xr.md b/camera/camerax/references/xr.md new file mode 100644 index 0000000..59fae32 --- /dev/null +++ b/camera/camerax/references/xr.md @@ -0,0 +1,54 @@ +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 + +| Concept | Mobile focus | **XR focus** | +|---|---|---| +| **Input** | Raw camera stream | **Spatial tracking (visual-inertial odometry (VIO) or simultaneous localization and mapping (SLAM))** | +| **Output** | Screen viewfinder | **Stereo passthrough and occlusion** | +| **Constraint** | Battery life | **Motion-to-photon latency (less than 20 ms)** | + +*** ** * ** *** + +## Follow the implementation guide + +### API selection + +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. +- **OEM SDKs**: Use manufacturer-specific libraries for hardware-accelerated passthrough. + +### Handle spatial passthrough + +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. + +```kotlin +// Example: Querying the spatial pose for the current camera frame +val headPose = xrSession.getHeadPose(frameTime) +val projectionMatrix = headPose.getProjectionMatrix(eyeIndex) +``` + +
+ +### Manage depth and occlusion + +Digital content must respect real-world depth to ensure accurate occlusion. + +- **Depth map** : Access raw depth data using `ARCore` or `SurfaceProcessor` to create an occlusion mask. +- **Hardware buffers** : Use `HardwareBuffer` to share camera frames directly with the GPU without CPU-side copies to minimize latency. + +*** ** * ** *** + +## XR pitfalls + +- **The nausea limit**: Any processing that delays the viewfinder by more than 20 ms causes user sickness. Don't perform image processing on the main thread. +- **Privacy restrictions** : Some XR devices return a black frame if you attempt to record the "Passthrough" layer. Check `Session.isRecordingSupported`. +- **Field of view (FOV)**: The camera FOV possibly doesn't match the display FOV. Use the SDK's projection matrixes instead of calculating aspect ratios manually. +- **Front buffer rendering** : If the device supports it, use `FrontBufferRenderer` for real-time overlays to bypass standard double-buffering latency. \ No newline at end of file diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 31d185f..784038e 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-06-09' + last-updated: '2026-07-02' keywords: - implementation - Android @@ -214,14 +214,8 @@ The request contains the following key information: - `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. Currently, 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. @@ -233,6 +227,9 @@ The request contains the following key information: 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). + ## Present the request to the user Present the user with the request, using the Credential Manager built-in UI. @@ -335,8 +332,8 @@ additional metadata as well along with verified email: } */ -> [!NOTE] -> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). +> [!IMPORTANT] +> **Important:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). ## Server-side validation for account creation @@ -400,10 +397,10 @@ standard passkey registration. ## WebView support -For the flow to work on a 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 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 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 e34dc18..9305e92 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 @@ -99,14 +99,8 @@ The request contains the following key information: - `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. Currently, 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. @@ -118,6 +112,9 @@ The request contains the following key information: 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). + ## Present the request to the user Present the user with the request, using the Credential Manager built-in UI. @@ -220,8 +217,8 @@ additional metadata as well along with verified email: } */ -> [!NOTE] -> **Note:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). +> [!IMPORTANT] +> **Important:** We highly recommend that after receiving the verified email, you trigger Credential Manager's [passkey creation](https://developer.android.com/identity/credential-manager/passkeys/create-passkeys). ## Server-side validation for account creation @@ -285,10 +282,10 @@ standard passkey registration. ## WebView support -For the flow to work on a 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 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/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md index 26e9f87..566374a 100644 --- a/jetpack-compose/adaptive/SKILL.md +++ b/jetpack-compose/adaptive/SKILL.md @@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-18' + last-updated: '2026-07-02' keywords: - android - ui 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 f1122fe..8b00b01 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 @@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts. `lib.versions.toml`. [versions] - compose = "1.12.0-beta01" + compose = "1.12.0-beta02" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 7625935..33b37a2 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 @@ -142,6 +142,83 @@ Grid( ![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-sizes.png) **Figure 3** . Row heights defined using the four primary track sizing options in `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. + +The following example allocates at least `100.dp` to the first row: + + +```kotlin +Grid( + config = { + column(1f) + // The first row has a minimum height of 100.dp and can expand to + // the half of the remaining space. + row(GridTrackSize.MinMax(100.dp, 1.fr)) + // The second row takes the half of the remaining space. + row(1.fr) + // The third row has a fixed height of 200.dp. + row(200.dp) + }, + modifier = Modifier.size(360.dp) // Total grid height is 360.dp +) { + PastelRedCard("MinMax(100.dp, 1.fr)") + PastelGreenCard("Flex(1.fr)") + PastelBlueCard("Fixed(200.dp)") +} +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/track-size-minmax.png) **Figure 4** . The first row has at least `100.dp` height. + +### 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). + +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( + config = { + column(1f) + // The first row's height is determined by the height of the Text composable. + row(GridTrackSize.Auto) + // The second row occupies the remaining space, allowing the LazyColumn to scroll. + row(GridTrackSize.MinMax(0.dp, 1.fr)) + + gap(8.dp) + }, + modifier = Modifier.size(width = 170.dp, height = 240.dp) +) { + Text("Lazy column in a Grid") + // The LazyColumn is placed in the second row, filling the remaining space. + LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) { + items(100) { number -> + PastelGreenCard("Card $number") + } + } +} +``` + +
+ +![Row heights defined using the four primary track sizing options.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/lazy-column-in-grid.png) **Figure 5** . `LazyColumn` in a grid cell. + ### Determine grid track size intrinsically You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` @@ -175,7 +252,7 @@ Grid(
-![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 4**. Intrinsic sizes specified in the columns. +![Intrinsic sizes specified in the columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/intrinsic-size.png) **Figure 5**. Intrinsic sizes specified in the columns. ## Set gaps between rows and columns @@ -211,7 +288,7 @@ Grid(
-![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 5**. Gaps between rows and columns. +![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, 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 e47dced..bb78a97 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 @@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co `lib.versions.toml`. [versions] - compose = "1.12.0-beta01" + compose = "1.12.0-beta02" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 77bc9da..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 @@ -68,7 +68,7 @@ Inspector](https://developer.android.com/static/develop/ui/compose/images/li-sho ### Compose semantics -In Compose, [Semantics](https://developer.android.com/develop/ui/compose/semantics) describe your UI in an +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 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 58a84c7..ea25f8e 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-06-20' + last-updated: '2026-07-02' keywords: - Jetpack Compose - migration @@ -56,7 +56,7 @@ following the logic in [references/identify-optimal-xml-candidate.md](references Analyze the identified XML View's structure, hierarchy, and implementation details. -Use [references/analysis-of-the-project-and-layout.md](https://developer.android.com/agents/skills/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout) to +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 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 new file mode 100644 index 0000000..3e2f0c3 --- /dev/null +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md @@ -0,0 +1,42 @@ +## 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. + +## 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. + +## 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. + +## 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). + +## 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. + +*** ** * ** *** + +> **Pro-tip:** Always prioritize the "Existing infrastructure" over "Default templates." If the project has a custom way of handling spacing or colors, composable code, or any other project layer, your generated Compose code must reflect that specific implementation. \ No newline at end of file diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index 8a82ccd..daf7256 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-06-11' + last-updated: '2026-07-02' keywords: - Jetpack Compose - Styles @@ -130,9 +130,15 @@ Refer to the official documentation to complete specific development tasks: For each custom component (for example, `CustomButton`), complete the following sequence: -1. If you are able to run an Android emulator, locate an existing screenshot test for the component. 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 Component. ELSE proceed to the next step without a screenshot test. +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. + - **If you CAN run an Android emulator:** Perform the following to capture a baseline screenshot: + - **Option A:** Locate and run an existing screenshot test for the component. + - **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. +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`. diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index 87ccd8b..3704be5 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-06-23' + last-updated: '2026-07-02' keywords: - recipe - Android @@ -55,7 +55,7 @@ Code examples showcasing common patterns. - *[Basic](references/android/guide/navigation/navigation-3/recipes/basic.md)*: Shows most basic API usage. - *[Saveable back stack](references/android/guide/navigation/navigation-3/recipes/basicsaveable.md)*: Shows basic API usage with a persistent back stack. -- *[Entry provider DSL](https://developer.android.com/guide/navigation/navigation-3/recipes/basicdsl)*: Shows basic API usage using the entryProvider DSL. +- *[Entry provider DSL](references/android/guide/navigation/navigation-3/recipes/basicdsl.md)*: Shows basic API usage using the entryProvider DSL. ### Common UI @@ -63,7 +63,7 @@ Code examples showcasing common patterns. ### Deep links -- *[Basic](https://developer.android.com/guide/navigation/navigation-3/recipes/deeplinks-basic)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. +- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. - *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior. ### Scenes @@ -98,7 +98,7 @@ Code examples showcasing common patterns. ### Architecture - *[Modularized navigation code (Hilt)](references/android/guide/navigation/navigation-3/recipes/modular-hilt.md)*: Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI. -- *[Modularized navigation code (Koin)](https://developer.android.com/guide/navigation/navigation-3/recipes/modular-koin)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI. +- *[Modularized navigation code (Koin)](references/android/guide/navigation/navigation-3/recipes/modular-koin.md)*: Demonstrates how to decouple navigation code into separate modules using Koin for DI. ### Working with ViewModel 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 new file mode 100644 index 0000000..2067c08 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md @@ -0,0 +1,85 @@ +# Basic DSL Recipe + +This recipe shows a basic example of how to use the Navigation 3 API with two screens, using the `entryProvider` DSL and a persistent back stack. + +## How it works + +This example is similar to the basic recipe, but with a few key differences: + +1. **Persistent Back Stack** : It uses `rememberNavBackStack(RouteA)` to create and remember the back stack. This makes the back stack persistent across configuration changes (e.g., screen rotation). To use `rememberNavBackStack`, the navigation keys must be serializable, which is why `RouteA` and `RouteB` are annotated with `@Serializable` and implement the `NavKey` interface. + +2. **`entryProvider` DSL** : Instead of a `when` statement, this example uses the `entryProvider` DSL to define the content for each route. The `entry` function is used to associate a route type with its composable content. + +The navigation logic remains the same: to navigate from `RouteA` to `RouteB`, we add a `RouteB` instance to the back stack. +[![](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/basicdsl) + +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.basicdsl + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentBlue +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data class RouteB(val id: String) : NavKey + +class BasicDslActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { + ContentGreen("Welcome to Nav3") { + Button(onClick = dropUnlessResumed { + backStack.add(RouteB("123")) + }) { + Text("Click to navigate") + } + } + } + entry { key -> + ContentBlue("Route id: ${key.id} ") + } + } + ) + } + } +} +``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md new file mode 100644 index 0000000..66312de --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md @@ -0,0 +1,744 @@ +# Deep Link Basic Recipe + +This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. + +## How it works + +It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. + +## Demonstrated forms of deeplink + +The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: + +1. `HomeKey` - deeplink with an exact url (no deeplink arguments) +2. `UsersKey` - deeplink with path arguments +3. `SearchKey` - deeplink with query arguments + +See `MainActivity.deepLinkPatterns` for the actual url pattern of each. + +## Recipe structure + +This recipe consists of three main packages: + +1. `basic.deeplink` - Contains the two activities +2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc +3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks + +[![](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/basic) + +``` +package com.example.nav3recipes.deeplink.basic + +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS +import kotlinx.serialization.Serializable + +internal interface NavRecipeKey: NavKey { + val name: String +} + +@Serializable +internal object HomeKey: NavRecipeKey { + override val name: String = STRING_LITERAL_HOME +} + +@Serializable +internal data class UsersKey( + val filter: String, +): NavRecipeKey { + override val name: String = STRING_LITERAL_USERS + companion object { + const val FILTER_KEY = STRING_LITERAL_FILTER + const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" + const val FILTER_OPTION_ALL = "all" + } +} + +@Serializable +internal data class SearchKey( + val firstName: String? = null, + val ageMin: Int? = null, + val ageMax: Int? = null, + val location: String? = null, +): NavRecipeKey { + override val name: String = STRING_LITERAL_SEARCH +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.net.toUri +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FriendsList +import com.example.nav3recipes.common.deeplink.LIST_USERS +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT +import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher +import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern +import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest +import com.example.nav3recipes.deeplink.basic.util.KeyDecoder +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * Parses a target deeplink into a NavKey. There are several crucial steps involved: + * + * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable + * format (see [DeepLinkPattern]) + * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) + * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each + * other to facilitate comparison and finding a match + * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match + * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result + * to backstack key, regardless of what the conversion method may be. + * STEP 4. Associate the match results with the correct backstack key + * + * This recipes provides an example for each of the above steps by way of kotlinx.serialization. + * + * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional + * deeplink considerations are not included in this scope** + * - Create synthetic backStack + * - Multi-modular setup + * - DI + * - Managing TaskStack + * - Up button ves Back Button + * + */ +class MainActivity : ComponentActivity() { + /** STEP 1. Parse supported deeplinks */ + // internal so that landing activity can link to this in the kdocs + internal val deepLinkPatterns: List> = listOf( + // "https://www.nav3recipes.com/home" + DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), + // "https://www.nav3recipes.com/users/with/{filter}" + DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), + // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" + DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), + ) + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + // retrieve the target Uri + val uri: Uri? = intent.data + // associate the target with the correct backstack key + val key: NavKey = uri?.let { + /** STEP 2. Parse requested deeplink */ + val request = DeepLinkRequest(uri) + /** STEP 3. Compared requested with supported deeplink to find match*/ + val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> + DeepLinkMatcher(request, pattern).match() + } + /** STEP 4. If match is found, associate match to the correct key*/ + match?.let { + //leverage kotlinx.serialization's Decoder to decode + // match result into a backstack key + KeyDecoder(match.args) + .decodeSerializableValue(match.serializer) + } + } ?: HomeKey // fallback if intent.uri is null or match is not found + + /** + * Then pass starting key to backstack + */ + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { key -> + EntryScreen(key.name) { + TextContent("") + } + } + entry { key -> + EntryScreen("${key.name} : ${key.filter}") { + TextContent("") + val list = when { + key.filter.isEmpty() -> LIST_USERS + key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS + else -> LIST_USERS.take(5) + } + FriendsList(list) + } + } + entry { search -> + EntryScreen(search.name) { + TextContent("") + val matchingUsers = LIST_USERS.filter { user -> + (search.firstName == null || user.firstName == search.firstName) && + (search.location == null || user.location == search.location) && + (search.ageMin == null || user.age >= search.ageMin) && + (search.ageMax == null || user.age <= search.ageMax) + } + FriendsList(matchingUsers) + } + } + } + ) + } + } +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.EMPTY +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE +import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY +import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM +import com.example.nav3recipes.common.deeplink.LOCATION_BC +import com.example.nav3recipes.common.deeplink.LOCATION_BR +import com.example.nav3recipes.common.deeplink.LOCATION_CA +import com.example.nav3recipes.common.deeplink.LOCATION_US +import com.example.nav3recipes.common.deeplink.MenuDropDown +import com.example.nav3recipes.common.deeplink.MenuTextInput +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE +import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE +import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * This activity allows the user to create a deep link and make a request with it. + * + * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct + * and trigger the deeplink request, and the [MainActivity] to show how an app can handle + * that request. + * + * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to + * demonstrate different types of supported deeplinks: + * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) + * 2. [UsersKey] - deeplink with path arguments + * 3. [SearchKey] - deeplink with query arguments + * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. + * + * **RECIPE STRUCTURE** This recipe consists of three main packages: + * 1. basic.deeplink - Contains the two activities + * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc + * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match + * the deeplinks + * + * See [MainActivity] for how the requested deeplink is handled. + */ +class CreateDeepLinkActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + /** + * UI for deeplink sandbox + */ + EntryScreen("Sandbox - Build Your Deeplink") { + TextContent("Base url:\n${PATH_BASE}/") + var showFilterOptions by remember { mutableStateOf(false) } + val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } + + var showQueryOptions by remember { mutableStateOf(false) } + var selectedFilter by remember { mutableStateOf("") } + val selectedSearchQuery = remember { mutableStateMapOf() } + + // manage path options + MenuDropDown( + menuOptions = MENU_OPTIONS_PATH, + ) { _, selection -> + selectedPath.value = selection + when (selection) { + PATH_SEARCH -> { + showQueryOptions = true + showFilterOptions = false + } + + PATH_INCLUDE -> { + showQueryOptions = false + showFilterOptions = true + } + + else -> { + showQueryOptions = false + showFilterOptions = false + } + } + } + + // manage path filter options, reset state if menu is closed + LaunchedEffect(showFilterOptions) { + selectedFilter = if (showFilterOptions) { + MENU_OPTIONS_FILTER.values.first().first() + } else { + "" + } + } + if (showFilterOptions) { + MenuDropDown( + menuOptions = MENU_OPTIONS_FILTER, + ) { _, selected -> + selectedFilter = selected + } + } + + // manage query options, reset state if menu is closed + LaunchedEffect(showQueryOptions) { + if (showQueryOptions) { + val initEntry = MENU_OPTIONS_SEARCH.entries.first() + selectedSearchQuery[initEntry.key] = initEntry.value.first() + } else { + selectedSearchQuery.clear() + } + } + if (showQueryOptions) { + MenuTextInput( + menuLabels = MENU_LABELS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + MenuDropDown( + menuOptions = MENU_OPTIONS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + } + + // form final deeplink url + val arguments = when (selectedPath.value) { + PATH_INCLUDE -> "/${selectedFilter}" + PATH_SEARCH -> { + buildString { + selectedSearchQuery.forEach { entry -> + if (entry.value.isNotEmpty()) { + val prefix = if (isEmpty()) "?" else "&" + append("$prefix${entry.key}=${entry.value}") + } + } + } + } + + else -> "" + } + val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" + TextContent("Final url:\n$finalUrl") + // deeplink to target + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent( + this@CreateDeepLinkActivity, + MainActivity::class.java + ) + // start activity with the url + intent.data = finalUrl.toUri() + startActivity(intent) + }) + } + } + } +} + +private const val KEY_PATH = "path" +private val MENU_OPTIONS_PATH = mapOf( + KEY_PATH to listOf( + STRING_LITERAL_HOME, + PATH_INCLUDE, + PATH_SEARCH, + ), +) + +private val MENU_OPTIONS_FILTER = mapOf( + UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), +) + +private val MENU_OPTIONS_SEARCH = mapOf( + SearchKey::firstName.name to listOf( + EMPTY, + FIRST_NAME_JOHN, + FIRST_NAME_TOM, + FIRST_NAME_MARY, + FIRST_NAME_JULIE + ), + SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) +) + +private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) + +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +/** + * Decodes the list of arguments into a a back stack key + * + * **IMPORTANT** This decoder assumes that all argument types are Primitives. + */ +@OptIn(ExperimentalSerializationApi::class) +internal class KeyDecoder( + private val arguments: Map, +) : AbstractDecoder() { + + override val serializersModule: SerializersModule = EmptySerializersModule() + private var elementIndex: Int = -1 + private var elementName: String = "" + + /** + * Decodes the index of the next element to be decoded. Index represents a position of the + * current element in the [descriptor] that can be found with [descriptor].getElementIndex. + * + * The returned index will trigger deserializer to call [decodeValue] on the argument at that + * index. + * + * The decoder continually calls this method to process the next available argument until this + * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more + * arguments to decode. + * + * This method should sequentially return the element index for every element that has its value + * available within [arguments]. + */ + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + var currentIndex = elementIndex + while (true) { + // proceed to next element + currentIndex++ + // if we have reached the end, let decoder know there are not more arguments to decode + if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE + val currentName = descriptor.getElementName(currentIndex) + // Check if bundle has argument value. If so, we tell decoder to process + // currentIndex. Otherwise, we skip this index and proceed to next index. + if (arguments.contains(currentName)) { + elementIndex = currentIndex + elementName = currentName + return elementIndex + } + } + } + + /** + * Returns argument value from the [arguments] for the argument at the index returned by + * [decodeElementIndex] + */ + override fun decodeValue(): Any { + val arg = arguments[elementName] + checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } + return arg + } + + override fun decodeNull(): Nothing? = null + + // we want to know if it is not null, so its !isNull + override fun decodeNotNullMark(): Boolean = arguments[elementName] != null +} +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri + +/** + * Parse the requested Uri and store it in a easily readable format + * + * @param uri the target deeplink uri to link to + */ +internal class DeepLinkRequest( + val uri: Uri +) { + /** + * A list of path segments + */ + val pathSegments: List = uri.pathSegments + + /** + * A map of query name to query value + */ + val queries = buildMap { + uri.queryParameterNames.forEach { argName -> + this[argName] = uri.getQueryParameter(argName)!! + } + } + + // TODO add parsing for other Uri components, i.e. fragments, mimeType, action +} +``` + +```` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.encoding.CompositeDecoder +import java.io.Serializable + +/** + * Parse a supported deeplink and stores its metadata as a easily readable format + * + * The following notes applies specifically to this particular sample implementation: + * + * The supported deeplink is expected to be built from a serializable backstack key [T] that + * supports deeplink. This means that if this deeplink contains any arguments (path or query), + * the argument name must match any of [T] member field name. + * + * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] + * supports two deeplink patterns: + * ``` + * val deeplink1 = www.nav3recipes.com/home + * val deeplink2 = www.nav3recipes.com/profile/{userId} + * ``` + * Then two [DeepLinkPattern] should be created + * ``` + * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) + * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) + * ``` + * + * This implementation assumes a few things: + * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match + * 2. all query arguments are optional by way of nullable/has default value + * + * @param T the backstack key type that supports the deeplinking of [uriPattern] + * @param serializer the serializer of [T] + * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" + */ +internal class DeepLinkPattern( + val serializer: KSerializer, + val uriPattern: Uri +) { + /** + * Help differentiate if a path segment is an argument or a static value + */ + private val regexPatternFillIn = Regex("\\{(.+?)\\}") + + // TODO make these lazy + /** + * parse the path into a list of [PathSegment] + * + * order matters here - path segments need to match in value and order when matching + * requested deeplink to supported deeplink + */ + val pathSegments: List = buildList { + uriPattern.pathSegments.forEach { segment -> + // first, check if it is a path arg + var result = regexPatternFillIn.find(segment) + if (result != null) { + // if so, extract the path arg name (the string value within the curly braces) + val argName = result.groups[1]!!.value + // from [T], read the primitive type of this argument to get the correct type parser + val elementIndex = serializer.descriptor.getElementIndex(argName) + if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { + throw IllegalArgumentException( + "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." + ) + } + + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + // finally, add the arg name and its respective type parser to the map + add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) + } else { + // if its not a path arg, then its just a static string path segment + add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) + } + } + } + + /** + * Parse supported queries into a map of queryParameterNames to [TypeParser] + * + * This will be used later on to parse a provided query value into the correct KType + */ + val queryValueParsers: Map = buildMap { + uriPattern.queryParameterNames.forEach { paramName -> + val elementIndex = serializer.descriptor.getElementIndex(paramName) + // Ignore static query parameters that are not in the Serializable class + if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + this[paramName] = getTypeParser(elementDescriptor.kind) + } + } + } + + /** + * Metadata about a supported path segment + */ + class PathSegment( + val stringValue: String, + val isParamArg: Boolean, + val typeParser: TypeParser + ) +} + +/** + * Parses a String into a Serializable Primitive + */ +private typealias TypeParser = (String) -> Serializable + +private fun getTypeParser(kind: SerialKind): TypeParser { + return when (kind) { + PrimitiveKind.STRING -> Any::toString + PrimitiveKind.INT -> String::toInt + PrimitiveKind.BOOLEAN -> String::toBoolean + PrimitiveKind.BYTE -> String::toByte + PrimitiveKind.CHAR -> String::toCharArray + PrimitiveKind.DOUBLE -> String::toDouble + PrimitiveKind.FLOAT -> String::toFloat + PrimitiveKind.LONG -> String::toLong + PrimitiveKind.SHORT -> String::toShort + else -> throw IllegalArgumentException( + "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." + ) + } +} +```` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.util.Log +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer + +internal class DeepLinkMatcher( + val request: DeepLinkRequest, + val deepLinkPattern: DeepLinkPattern +) { + /** + * Match a [DeepLinkRequest] to a [DeepLinkPattern]. + * + * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise + */ + fun match(): DeepLinkMatchResult? { + if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null + if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null + if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null + // exact match (url does not contain any arguments) + if (request.uri == deepLinkPattern.uriPattern) + return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) + + val args = mutableMapOf() + // match the path + request.pathSegments + .asSequence() + // zip to compare the two objects side by side, order matters here so we + // need to make sure the compared segments are at the same position within the url + .zip(deepLinkPattern.pathSegments.asSequence()) + .forEach { it -> + // retrieve the two path segments to compare + val requestedSegment = it.first + val candidateSegment = it.second + // if the potential match expects a path arg for this segment, try to parse the + // requested segment into the expected type + if (candidateSegment.isParamArg) { + val parsedValue = try { + candidateSegment.typeParser.invoke(requestedSegment) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) + return null + } + args[candidateSegment.stringValue] = parsedValue + } else if(requestedSegment != candidateSegment.stringValue){ + // if it's path arg is not the expected type, its not a match + return null + } + } + // match queries (if any) + request.queries.forEach { query -> + val name = query.key + // If the pattern does not define this query parameter, ignore it. + // This prevents a NullPointerException. + val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach + + val queryParsedValue = try { + queryStringParser.invoke(query.value) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) + return null + } + args[name] = queryParsedValue + } + // provide the serializer of the matching key and map of arg names to parsed arg values + return DeepLinkMatchResult(deepLinkPattern.serializer, args) + } +} + + +/** + * Created when a requested deeplink matches with a supported deeplink + * + * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink + * @param serializer serializer for [T] + * @param args The map of argument name to argument value. The value is expected to have already + * been parsed from the raw url string back into its proper KType as declared in [T]. + * Includes arguments for all parts of the uri - path, query, etc. + * */ +internal data class DeepLinkMatchResult( + val serializer: KSerializer, + val args: Map +) + +const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" +``` + +``` +package com.example.nav3recipes.deeplink.basic.ui + +import com.example.nav3recipes.deeplink.basic.SearchKey + +/** + * String resources + */ +internal const val STRING_LITERAL_FILTER = "filter" +internal const val STRING_LITERAL_HOME = "home" +internal const val STRING_LITERAL_USERS = "users" +internal const val STRING_LITERAL_SEARCH = "search" +internal const val STRING_LITERAL_INCLUDE = "include" +internal const val PATH_BASE = "https://www.nav3recipes.com" +internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" +internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" +internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" + +internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" +internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + + "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + + "&${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/modular-koin.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md new file mode 100644 index 0000000..d1e7ad6 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md @@ -0,0 +1,287 @@ +# Modular Navigation Recipe (Koin) + +This recipe demonstrates how to structure a multi-module application using Navigation 3 and Koin for dependency injection. The goal is to create a decoupled architecture where navigation is defined and implemented in separate feature modules. It relies on the [`koin-compose-navigation3`](https://insert-koin.io/docs/reference/koin-compose/navigation3) artifact. + +## How it works + +The application is divided into several Android modules: + +- **`app` module** : This is the main application module. It `includes()` the feature modules and initializes a common `Navigator`. + +- **`common` module** : This module contains the core navigation logic used by both the application module and the feature modules. Namely, it defines a `Navigator` class that manages the back stack. + +- **Feature modules (e.g., `conversation`, `profile`)**: Each feature is split into two sub-modules: + + - **`api` module**: Defines the public API for the feature, including its navigation routes. This allows other modules to navigate to this feature without needing to know about its implementation details. + - **`impl` module** : Provides the implementation of the feature, including its composables and Koin `Module`. The Koin module uses the [`navigation`](https://insert-koin.io/docs/reference/koin-compose/navigation3/#declaring-navigation-entries) DSL to define the entry provider installers for the feature module. + +This modular approach allows for a clean separation of concerns, making the codebase more scalable and maintainable. Each feature is responsible for its own navigation logic, and the `app` module only combines these pieces together. +[![](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/modular/koin) + +``` +package com.example.nav3recipes.modular.koin + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.dsl.module +import org.koin.dsl.navigation3.navigation + +// API +object Profile + +// IMPL +@OptIn(KoinExperimentalAPI::class) +val profileModule = module { + activityRetainedScope { + navigation { ProfileScreen() } + } +} + +@Composable +private fun ProfileScreen() { + val profileColor = MaterialTheme.colorScheme.surfaceVariant + Column( + modifier = Modifier + .fillMaxSize() + .background(profileColor) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Profile Screen", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.ui.theme.colors +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.dsl.module +import org.koin.dsl.navigation3.navigation + +// API +object ConversationList +data class ConversationDetail(val id: Int) { + val color: Color + get() = colors[id % colors.size] +} + +// IMPL +@OptIn(KoinExperimentalAPI::class) +val conversationModule = module { + activityRetainedScope { + navigation { + ConversationListScreen( + onConversationClicked = { conversationDetail -> + get().goTo(conversationDetail) + } + ) + } + + navigation { key -> + ConversationDetailScreen(key) { + get().goTo(Profile) + } + } + } +} + +@Composable +private fun ConversationListScreen( + onConversationClicked: (ConversationDetail) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + items(10) { index -> + val conversationId = index + 1 + val conversationDetail = ConversationDetail(conversationId) + val backgroundColor = conversationDetail.color + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = dropUnlessResumed { + onConversationClicked(conversationDetail) + }), + headlineContent = { + Text( + text = "Conversation $conversationId", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + colors = ListItemDefaults.colors( + containerColor = backgroundColor // Set container color directly + ) + ) + } + } +} + +@Composable +private fun ConversationDetailScreen( + conversationDetail: ConversationDetail, + onProfileClicked: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(conversationDetail.color) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Conversation Detail Screen: ${conversationDetail.id}", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed(block = onProfileClicked)) { + Text("View Profile") + } + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList + +class Navigator(startDestination: Any) { + val backStack : SnapshotStateList = mutableStateListOf(startDestination) + + fun goTo(destination: Any){ + backStack.add(destination) + } + + fun goBack(){ + backStack.removeLastOrNull() + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import org.koin.androidx.scope.dsl.activityRetainedScope +import org.koin.dsl.module + +val appModule = module { + includes(profileModule,conversationModule) + + activityRetainedScope { + scoped { + Navigator(startDestination = ConversationList) + } + } +} +``` + +``` +package com.example.nav3recipes.modular.koin + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import org.koin.android.ext.android.inject +import org.koin.android.scope.AndroidScopeComponent +import org.koin.androidx.compose.navigation3.getEntryProvider +import org.koin.androidx.scope.activityRetainedScope +import org.koin.core.Koin +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.core.component.KoinComponent +import org.koin.core.scope.Scope +import org.koin.dsl.koinApplication + +/** + * This recipe demonstrates how to use a modular approach with Navigation 3, + * where different parts of the application are defined in separate modules and injected + * into the main app using Koin. + * + * Features (Conversation and Profile) are split into two modules: + * - api: defines the public facing routes for this feature + * - impl: defines the entryProviders for this feature, these are injected into the app's main activity + * The common module defines: + * - a common navigator class that exposes a back stack and methods to modify that back stack + * - a type that should be used by feature modules to inject entryProviders into the app's main activity + * The app module creates the navigator by supplying a start destination and provides this navigator + * to the rest of the app module (i.e. MainActivity) and the feature modules. + */ +@OptIn(KoinExperimentalAPI::class) +class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComponent { + // Local Koin Context Instance + companion object { + private val localKoin = koinApplication { + modules(appModule) + }.koin + } + // Override default Koin context to use the local one + override fun getKoin(): Koin = localKoin + override val scope : Scope by activityRetainedScope() + val navigator: Navigator by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setEdgeToEdgeConfig() + setContent { + Scaffold { paddingValues -> + NavDisplay( + backStack = navigator.backStack, + modifier = Modifier.padding(paddingValues), + onBack = { navigator.goBack() }, + entryProvider = getEntryProvider() + ) + } + } + } + +} +``` \ No newline at end of file diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 2c9a9d7..afdb402 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-06-19' + last-updated: '2026-07-02' keywords: - android - engage @@ -32,7 +32,7 @@ Follow these steps to assist the developer: - Always refer to [common.md](references/common.md) for common entities. - Ask which cluster type they want to publish from the supported cluster types for that vertical. - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. - - Get the request structure from [requests.md](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/requests) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. + - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. 2. **Generate Structured Boilerplate Code:** - Create a new directory for all Engage-related code. Name the directory to match the naming convention of the existing codebase. 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 2afe8d9..9218a8a 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,5 +1,8 @@
+[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: @@ -12,7 +15,7 @@ of the TV features: Before you begin, complete the following steps: -1. [Express interest in developing with Engage](http://g.co/tv/vda) to enroll in +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 diff --git a/play/engage-sdk-integration/references/patterns.md b/play/engage-sdk-integration/references/patterns.md index bc7f214..be290a9 100644 --- a/play/engage-sdk-integration/references/patterns.md +++ b/play/engage-sdk-integration/references/patterns.md @@ -137,7 +137,7 @@ class EngageWorker(context: Context, workerParams: WorkerParameters) : Coroutine return when (publishType) { Constants.PUBLISH_TYPE_RECOMMENDATIONS -> publishRecommendations() // Constants.PUBLISH_TYPE_FEATURED -> publishFeatured() - // Constants.PUBLISH_TYPE_CONTINUATION-> publishContinuation() + Constants.PUBLISH_TYPE_CONTINUATION -> publishContinuation() Constants.PUBLISH_TYPE_USER_ACCOUNT_MANAGEMENT -> publishUserAccountManagement() else -> Result.failure() } @@ -152,6 +152,21 @@ class EngageWorker(context: Context, workerParams: WorkerParameters) : Coroutine return publishAndProvideResult(publishTask) } + private suspend fun publishContinuation(): Result { + // Empty Continuation Guard: If there is no continuation content, + // we must delete the cluster instead of publishing an empty one on the UI. + if (getContinuationData().isEmpty()) { + val deleteTask = client.deleteContinuationCluster() + return publishAndProvideResult(deleteTask) + } + + val publishTask: Task = + client.publishContinuationCluster( + clusterRequestFactory.constructContinuationClusterRequest() + ) + return publishAndProvideResult(publishTask) + } + private suspend fun publishUserAccountManagement(): Result { val publishTask: Task if (isAccountSignedIn()) { @@ -181,6 +196,11 @@ class EngageWorker(context: Context, workerParams: WorkerParameters) : Coroutine // ... } + private fun getContinuationData(): List { + // Implement your app's data loading logic here. + // ... + } + private suspend fun publishAndProvideResult( publishTask: Task ): Result { @@ -291,11 +311,28 @@ class ClusterRequestFactory(context: Context) { val items = appDataRepository.getRecommendations() val recommendationCluster = com.google.android.engage.common.datamodel.RecommendationCluster.Builder() + .setTitle("Recommended Content") // Required field + .setRecommendationClusterType(com.google.android.engage.common.datamodel.RecommendationClusterType.TYPE_TOP_PICKS_FOR_YOU) // Required field for (item in items) { recommendationCluster.addEntity(ItemToEntityConverter.convert(item)) } return com.google.android.engage.service.PublishRecommendationClustersRequest.Builder() .addRecommendationCluster(recommendationCluster.build()) + .setAccountProfile(accountProfile) // Set the account profile on the request for personalization/sync + .build() + } + + fun constructContinuationClusterRequest(): com.google.android.engage.service.PublishContinuationClusterRequest { + val items = appDataRepository.getContinuationData() + + val continuationCluster = com.google.android.engage.common.datamodel.ContinuationCluster.Builder() + .setAccountProfile(accountProfile) // Set the account profile on the request for personalization/sync + + for (item in items) { + continuationCluster.addEntity(ItemToEntityConverter.convert(item)) + } + return com.google.android.engage.service.PublishContinuationClusterRequest.Builder() + .setContinuationCluster(continuationCluster.build()) .build() } @@ -344,7 +381,7 @@ object Constants { const val PUBLISH_TYPE_KEY = "PUBLISH_TYPE" const val PUBLISH_TYPE_RECOMMENDATIONS = "RECOMMENDATIONS" const val PUBLISH_TYPE_FEATURED = "FEATURED" - // const val PUBLISH_TYPE_CONTINUATION = "CONTINUATION" + const val PUBLISH_TYPE_CONTINUATION = "CONTINUATION" // ... const val PUBLISH_TYPE_USER_ACCOUNT_MANAGEMENT = "USER_ACCOUNT_MANAGEMENT" // const val PUBLISH_TYPE_FOOD_SHOPPING_CARD = "FOOD_SHOPPING_CARD" diff --git a/play/engage-sdk-integration/references/requests.md b/play/engage-sdk-integration/references/requests.md new file mode 100644 index 0000000..c65f0a6 --- /dev/null +++ b/play/engage-sdk-integration/references/requests.md @@ -0,0 +1,231 @@ +This file defines the request structures for publishing various data models in +the Engage SDK. + + { + "PublishRecommendationClustersRequest": { + "package": "com.google.android.engage.service.PublishRecommendationClustersRequest", + "fields": { + "recommendationClusters": { + "type": "List", + "requirement": "Required", + "adder": "addRecommendationCluster(RecommendationCluster)", + "getter": "getRecommendationClusters()" + }, + "accountProfile": { + "type": "@NonNull AccountProfile", + "requirement": "Optional", + "setter": "setAccountProfile(@NonNull AccountProfile)", + "getter": "getAccountProfile()" + }, + "syncAcrossDevices": { + "type": "Boolean", + "requirement": "Optional", + "setter": "setSyncAcrossDevices(boolean)", + "getter": "getSyncAcrossDevices()" + } + } + }, + "PublishFeaturedClusterRequest": { + "package": "com.google.android.engage.service.PublishFeaturedClusterRequest", + "fields": { + "featuredCluster": { + "type": "FeaturedCluster", + "requirement": "Required", + "setter": "setFeaturedCluster(FeaturedCluster)", + "getter": "getFeaturedCluster()" + } + } + }, + "DeleteClustersRequest": { + "package": "com.google.android.engage.service.DeleteClustersRequest", + "fields": { + "clusterTypes": { + "type": "List<@ClusterType int>", + "requirement": "Optional", + "adder": "addClusterType(@ClusterType int)" + }, + "deleteReason": { + "type": "@DeleteReason int", + "requirement": "Optional", + "setter": "setDeleteReason(@DeleteReason int)", + "getter": "getDeleteReason()" + }, + "accountProfile": { + "requirement": "Optional", + "setter": "setAccountProfile(AccountProfile)", + "type": "AccountProfile", + "getter": "getAccountProfile()" + }, + "syncAcrossDevices": { + "requirement": "Optional", + "setter": "setSyncAcrossDevices(boolean)", + "type": "Boolean", + "getter": "getSyncAcrossDevices()" + } + } + }, + "PublishContinuationClusterRequest": { + "package": "com.google.android.engage.service.PublishContinuationClusterRequest", + "fields": { + "continuationCluster": { + "type": "ContinuationCluster", + "requirement": "Required", + "setter": "setContinuationCluster(ContinuationCluster)", + "getter": "getContinuationCluster()" + } + } + }, + "PublishStatusRequest": { + "package": "com.google.android.engage.service.PublishStatusRequest", + "fields": { + "statusCode": { + "type": "@AppEngagePublishStatusCode int", + "requirement": "Required", + "setter": "setStatusCode(@AppEngagePublishStatusCode int)", + "getter": "getStatusCode()" + } + } + }, + "PublishSubscriptionRequest": { + "package": "com.google.android.engage.service.PublishSubscriptionRequest", + "fields": { + "subscriptionClusters": { + "type": "List", + "requirement": "Required" + }, + "accountProfile": { + "type": "AccountProfile", + "requirement": "Required", + "setter": "setAccountProfile(AccountProfile)", + "getter": "getAccountProfile()" + }, + "subscription": { + "requirement": "Required", + "setter": "setSubscription(SubscriptionEntity)", + "type": "SubscriptionEntity", + "getter": "getSubscription()" + } + } + }, + "PublishUserAccountManagementRequest": { + "package": "com.google.android.engage.service.PublishUserAccountManagementRequest", + "fields": { + "actionUri": { + "type": "Uri", + "requirement": "Required" + }, + "signInCardEntity": { + "type": "SignInCardEntity", + "requirement": "Required", + "setter": "setSignInCardEntity(SignInCardEntity)" + }, + "userSettingsCardEntity": { + "requirement": "Required", + "setter": "setUserSettingsCardEntity(UserSettingsCardEntity)", + "type": "UserSettingsCardEntity" + } + } + }, + "PublishShoppingCartClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingCartClusterRequest", + "fields": { + "shoppingCart": { + "requirement": "Required", + "setter": "setShoppingCart(ShoppingCart)", + "type": "ShoppingCart", + "getter": "getShoppingCart()" + } + } + }, + "PublishShoppingListsRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingListsRequest", + "fields": { + "shoppingLists": { + "type": "List", + "requirement": "Optional", + "adder": "addShoppingList(ShoppingList)", + "getter": "getShoppingLists()", + "adderAll": "addShoppingLists(List)" + } + } + }, + "PublishShoppingOrderTrackingClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingOrderTrackingClusterRequest", + "fields": { + "shoppingOrderTrackingCluster": { + "type": "ShoppingOrderTrackingCluster", + "requirement": "Required", + "setter": "setShoppingOrderTrackingCluster(ShoppingOrderTrackingCluster)", + "getter": "getShoppingOrderTrackingCluster()" + } + } + }, + "PublishShoppingReorderClusterRequest": { + "package": "com.google.android.engage.shopping.service.PublishShoppingReorderClusterRequest", + "fields": { + "reorderCluster": { + "type": "ShoppingReorderCluster", + "requirement": "Required", + "setter": "setReorderCluster(ShoppingReorderCluster)", + "getter": "getReorderCluster()" + } + } + }, + "PublishFoodShoppingCartsRequest": { + "package": "com.google.android.engage.food.service.PublishFoodShoppingCartsRequest", + "fields": { + "foodShoppingCarts": { + "type": "List", + "requirement": "Optional", + "adder": "addFoodShoppingCart(FoodShoppingCart)", + "getter": "getFoodShoppingCarts()", + "adderAll": "addFoodShoppingCarts(List)" + } + } + }, + "PublishFoodShoppingListsRequest": { + "package": "com.google.android.engage.food.service.PublishFoodShoppingListsRequest", + "fields": { + "foodShoppingLists": { + "type": "List", + "requirement": "Optional", + "adder": "addFoodShoppingList(FoodShoppingList)", + "getter": "getFoodShoppingLists()", + "adderAll": "addFoodShoppingLists(List)" + } + } + }, + "PublishReorderClusterRequest": { + "package": "com.google.android.engage.food.service.PublishReorderClusterRequest", + "fields": { + "reorderCluster": { + "requirement": "Required", + "setter": "setReorderCluster(FoodReorderCluster)", + "type": "FoodReorderCluster", + "getter": "getReorderCluster()" + } + } + }, + "PublishContinueSearchClusterRequest": { + "package": "com.google.android.engage.travel.service.PublishContinueSearchClusterRequest", + "fields": { + "continueSearchCluster": { + "type": "ContinueSearchCluster", + "requirement": "Required", + "setter": "setContinueSearchCluster(ContinueSearchCluster)", + "getter": "getContinueSearchCluster()" + } + } + }, + "PublishReservationClusterRequest": { + "package": "com.google.android.engage.travel.service.PublishReservationClusterRequest", + "fields": { + "reservationCluster": { + "type": "ReservationCluster", + "requirement": "Required", + "setter": "setReservationCluster(ReservationCluster)", + "getter": "getReservationCluster()" + } + } + } + } \ 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 83392ea..6784606 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-06-19' + last-updated: '2026-07-02' keywords: - android - play billing diff --git a/security/android-intent-security/SKILL.md b/security/android-intent-security/SKILL.md index ac09e4a..a94df09 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-06-24' + last-updated: '2026-06-25' keywords: - recipe - Android @@ -24,13 +24,6 @@ metadata: - Best Practices --- -auditing component configurations in AndroidManifest.xml (activities, services, -receivers) or source code handling incoming Intents (getIntent, -getParcelableExtra) to prevent Intent Redirection and unauthorized access. -keywords_public: recipe, Android, Security, Intent, Redirection, PendingIntent, -ContentProvider, Service ,Signature, Verification, Sanitizer, Vulnerability, -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. diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md index d92e36f..31207c7 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-06-02' + last-updated: '2026-06-25' keywords: - android - testing 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 3ae3f34..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 @@ -21,7 +21,7 @@ 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)). +an empty activity such as [`ComponentActivity`](https://developer.android.com/reference/androidx/activity/ComponentActivity). class MyComposeTest { diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md index 861d2c4..2cb5b96 100644 --- a/wear/wear-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -8,7 +8,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-18' + last-updated: '2026-07-02' keywords: - Wear OS - Compose diff --git a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md index 1969a7d..70af9c6 100644 --- a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5: ### M3 - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha04") + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha05") See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). -Wear Compose Foundation library version 1.7.0-alpha04 introduced +Wear Compose Foundation library version 1.7.0-alpha05 introduced some new components that are designed to work with Material 3 components. Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an updated animation when running on Wear OS 6 (API level 36) or higher. When updating to Wear Compose Material 3 version, we suggest to also update the Wear Compose Foundation and Navigation libraries: - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha04") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha04") + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha05") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha05") ## Theme @@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components: And finally a list of some relevant components from Wear Compose Foundation library: -| Wear Compose Foundation 1.7.0-alpha04 | | +| Wear Compose Foundation 1.7.0-alpha05 | | |---|---| | [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | | [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 449394c..63ce459 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-06-23' + last-updated: '2026-07-02' keywords: - Jetpack Compose Glimmer - audio glasses @@ -207,7 +207,7 @@ If you are creating a Glimmer Button component, read the: - **Developer Guidance:** [Jetpack Compose Glimmer: Buttons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md) - **API Source Code:** Use [references/button-source.md](references/button-source.md). -- **Implementation Samples:** Use [references/button-samples-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source). +- **Implementation Samples:** Use [references/button-samples-source.md](references/button-samples-source.md). #### Title Chips @@ -277,7 +277,7 @@ If you are creating a Glimmer List component, read the: - **API Source Code (List):** Use [references/list-source.md](references/list-source.md). - **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). -- **API Source Code (ListState):** Use [references/liststate-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source). +- **API Source Code (ListState):** Use [references/liststate-source.md](references/liststate-source.md). #### Stacks diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md new file mode 100644 index 0000000..fba367d --- /dev/null +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md @@ -0,0 +1,176 @@ +When creating a Glimmer Button component, refer to the following implementation +samples in `ButtonSamples.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.glimmer.Button +import androidx.xr.glimmer.ButtonSize +import androidx.xr.glimmer.GlimmerTheme +import androidx.xr.glimmer.Icon +import androidx.xr.glimmer.Text +import androidx.xr.glimmer.list.GlimmerLazyColumn + +@Composable +fun ButtonSampleUsage() { + GlimmerLazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), + ) { + item { ButtonSample() } + item { ButtonWithLeadingIconSample() } + item { ButtonWithTrailingIconSample() } + item { ButtonWithLeadingAndTrailingIconSample() } + item { LargeButtonSample() } + item { LargeButtonWithLeadingIconSample() } + item { LargeButtonWithTrailingIconSample() } + item { LargeButtonWithLeadingAndTrailingIconSample() } + } +} + +@Sampled +@Composable +fun ButtonSample() { + Button(onClick = {}) { Text("Send") } +} + +@Sampled +@Composable +fun ButtonWithLeadingIconSample() { + Button(onClick = {}, leadingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") + } +} + +@Composable +private fun ButtonWithTrailingIconSample() { + Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { + Text("Send") + } +} + +@Composable +private fun ButtonWithLeadingAndTrailingIconSample() { + Button( + onClick = {}, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Sampled +@Composable +fun LargeButtonSample() { + Button(onClick = {}, buttonSize = ButtonSize.Large) { Text("Send") } +} + +@Composable +private fun LargeButtonWithLeadingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Composable +private fun LargeButtonWithTrailingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Composable +private fun LargeButtonWithLeadingAndTrailingIconSample() { + Button( + onClick = {}, + buttonSize = ButtonSize.Large, + leadingIcon = { Icon(FavoriteIcon, "Localized description") }, + trailingIcon = { Icon(FavoriteIcon, "Localized description") }, + ) { + Text("Send") + } +} + +@Preview +@Composable +private fun ButtonPreview() { + GlimmerTheme { ButtonSample() } +} + +@Preview +@Composable +private fun ButtonWithLeadingIconPreview() { + GlimmerTheme { ButtonWithLeadingIconSample() } +} + +@Preview +@Composable +private fun ButtonWithTrailingIconPreview() { + GlimmerTheme { ButtonWithTrailingIconSample() } +} + +@Preview +@Composable +private fun ButtonWithLeadingAndTrailingIconPreview() { + GlimmerTheme { ButtonWithLeadingAndTrailingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonPreview() { + GlimmerTheme { LargeButtonSample() } +} + +@Preview +@Composable +private fun LargeButtonWithLeadingIconPreview() { + GlimmerTheme { LargeButtonWithLeadingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonWithTrailingIconPreview() { + GlimmerTheme { LargeButtonWithTrailingIconSample() } +} + +@Preview +@Composable +private fun LargeButtonWithLeadingAndTrailingIconPreview() { + GlimmerTheme { LargeButtonWithLeadingAndTrailingIconSample() } +} +``` + +
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md index 1586776..676f68a 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md @@ -33,6 +33,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.glimmer.ActionCard import androidx.xr.glimmer.Button import androidx.xr.glimmer.Card import androidx.xr.glimmer.GlimmerTheme @@ -47,9 +48,9 @@ fun CardSampleUsage() { item { CardWithTrailingIconSample() } item { CardWithTitleAndSubtitleAndLeadingIconSample() } item { CardWithTitleAndHeaderSample() } - item { CardWithTitleAndActionSample() } + item { ActionCardWithTitleSample() } item { CardWithTitleAndLeadingIconAndHeader() } - item { CardWithTitleAndLeadingIconAndHeaderAndAction() } + item { ActionCardWithTitleAndLeadingIconAndHeader() } item { CardWithLongText() } item { CardWithTitleAndSubtitleAndLeadingIconLongText() } item { CardWithTitleAndSubtitleAndLeadingIconAndTrailingIconLongText() } @@ -97,8 +98,8 @@ fun CardWithTitleAndHeaderSample() { @Sampled @Composable -fun CardWithTitleAndActionSample() { - Card(action = { Button(onClick = {}) { Text("Send") } }, title = { Text("Title") }) { +fun ActionCardWithTitleSample() { + ActionCard(action = { Button(onClick = {}) { Text("Send") } }, title = { Text("Title") }) { Text("This is a card with a title and action") } } @@ -158,8 +159,8 @@ fun CardWithTitleAndLeadingIconAndHeader() { } @Composable -fun CardWithTitleAndLeadingIconAndHeaderAndAction() { - Card( +fun ActionCardWithTitleAndLeadingIconAndHeader() { + ActionCard( action = { Button(onClick = {}, trailingIcon = { Icon(FavoriteIcon, "Localized description") }) { Text("Send") @@ -239,8 +240,8 @@ private fun CardWithTitleAndHeaderPreview() { @Preview @Composable -private fun CardWithTitleAndActionPreview() { - GlimmerTheme { CardWithTitleAndActionSample() } +private fun ActionCardWithTitlePreview() { + GlimmerTheme { ActionCardWithTitleSample() } } @Preview @@ -251,8 +252,8 @@ private fun CardWithTitleAndLeadingIconAndHeaderPreview() { @Preview @Composable -private fun CardWithTitleAndLeadingIconAndHeaderAndActionPreview() { - GlimmerTheme { CardWithTitleAndLeadingIconAndHeaderAndAction() } +private fun ActionCardWithTitleAndLeadingIconAndHeaderPreview() { + GlimmerTheme { ActionCardWithTitleAndLeadingIconAndHeader() } } @Preview diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md index c260bfe..9c290c9 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/card-source.md @@ -242,21 +242,21 @@ public fun Card( } /** - * Card is a component used to group related information into a single digestible unit. A card can - * adapt to display a wide range of content, from simple text blurbs to more complex summaries with - * multiple elements. A card contains text [content], and may also have any combination of [title], - * [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is placed on top of the - * [subtitle], which is placed on top of the [content]. A card fills the maximum width available by - * default. + * ActionCard is a version of a card that contains a primary [action] that is placed in the center + * of the bottom edge of the card. The action should be a [Button], and represents the action that + * will be performed when this card is interacted with. The main card itself is not focusable - the + * [action] takes the focus instead. * - * This Card contains an [action] that is placed on the center of the bottom edge of the card. The - * action should be a [Button], and represents the action that will be performed when this card is - * interacted with. The main card itself is not focusable - the [action] takes the focus instead. + * ActionCard is a component used to group related information into a single digestible unit. An + * action card can adapt to display a wide range of content, from simple text blurbs to more complex + * summaries with multiple elements. An action card contains text [content], and may also have any + * combination of [title], [subtitle], [leadingIcon], and [trailingIcon]. If specified, [title] is + * placed on top of the [subtitle], which is placed on top of the [content]. An action card fills + * the maximum width available by default. * - * For more documentation and samples of the other card parameters, see the other card overload - * without an action. + * For more documentation and samples of the other cards, see [Card]. * - * @sample androidx.xr.glimmer.samples.CardWithTitleAndActionSample + * @sample androidx.xr.glimmer.samples.ActionCardWithTitleSample * @param action the action for this card. This should be a [Button], and represents the action * performed when a user interacts with this card. The action is placed overlapping the bottom * edge of the card. @@ -285,7 +285,7 @@ public fun Card( * be limited to 10 lines of text. */ @Composable -public fun Card( +public fun ActionCard( action: @Composable () -> Unit, modifier: Modifier = Modifier, title: @Composable (() -> Unit)? = null, diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md new file mode 100644 index 0000000..134df7c --- /dev/null +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md @@ -0,0 +1,420 @@ +When creating a Glimmer List component, refer to the following source code in +`ListState.kt` for creating a state for the list: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer.list + +import androidx.annotation.IntRange +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.ScrollIndicatorState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList +import androidx.compose.runtime.Composable +import androidx.compose.runtime.annotation.FrequentlyChangingValue +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.Remeasurement +import androidx.compose.ui.layout.RemeasurementModifier +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.xr.glimmer.list.ListState.Companion.Saver +import kotlin.math.abs + +/** + * Creates a [ListState] that is remembered across compositions. + * + * Changes to the provided initial values will **not** result in the state being recreated or + * changed in any way if it has already been created. + * + * @param initialFirstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param initialFirstVisibleItemScrollOffset the initial value for + * [ListState.firstVisibleItemScrollOffset] + */ +@Composable +public fun rememberListState( + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Int = 0, +): ListState = + rememberSaveable(saver = ListState.Saver) { + ListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) + } + +/** + * A state object that can be hoisted to control and observe scrolling. + * + * In most cases, this will be created via [rememberListState]. + * + * @param firstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param firstVisibleItemScrollOffset the initial value for + * [ListState.firstVisibleItemScrollOffset] + */ +public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) : + ScrollableState { + + private val backingState = ScrollableState { -onScroll(-it) } + + // TODO: b/414961654 - Consider making this abstraction around "anchor item". + /** The holder class for the current scroll position. */ + private val scrollPosition = + GlimmerListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) + + /** Backing state for [layoutInfo] */ + internal val layoutInfoState = mutableStateOf(EmptyLazyListMeasureResult, neverEqualPolicy()) + + private val density: Density + get() = layoutInfoState.value.density + + /** + * This field is used to save information about the number of "beyond bounds items" that we want + * to compose. These items are not within the visible bounds of the lazy layout, but we compose + * them because they are explicitly requested through the + * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout]. + */ + internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() + + /** Includes information for requesting focus for children as the list scrolls. */ + internal val autoFocusState = GlimmerListAutoFocusState() + + /** Stores currently pinned items which are always composed. */ + internal val pinnedItems = LazyLayoutPinnedItemList() + + internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource() + + /** + * The scroll amount was provided in `onScroll` to be consumed during the current measure pass. + * + * Scrolling forward is negative. + */ + internal var incomingScroll: Float = 0f + private set + + /** + * This value retains the scroll amount that we want to carry over between measure passes. It + * represents the amount of scroll from previous passes that wasn't consumed back then but is + * awaiting to be consumed later. Together with [incomingScroll], this represents the total + * scroll available to the list during the current measure pass. + * + * This variable exists because the layout uses integer pixels while [onScroll] operates with + * fractional floats. Consequently, the list might receive tiny fractions of scroll input which, + * when aggregated, should equate to a few pixels of scrolling. However, because these small + * fractions are spread across many [onScroll] invocations, the list ignores them, making it + * feel unresponsive during slow, gentle touches. To avoid this, instead of ignoring these tiny + * scroll increments, the list accumulates them so they can be added to the next scroll part and + * actually consumed in a subsequent pass. + * + * Note that this value can sometimes be opposite to the scroll direction. This occurs because + * the [Float] -> [Int] rounding causes us to consume more than was provided. To compensate, we + * add a negative value to [incomingScroll] the next time. + * + * Scrolling forward is negative. + */ + internal var carryOverScroll: Float = 0f + private set + + /** + * This value is updated after the measure pass inside [applyMeasureResult] and defines how much + * of the [incomingScroll] was actually used. + */ + private var consumedScroll: Float = 0f + + internal val nearestRange: kotlin.ranges.IntRange by + LazyLayoutNearestRangeState(0, NearestItemsSlidingWindowSize, NearestItemsExtraItemCount) + + /** + * The [Remeasurement] object associated with our layout. It allows us to remeasure + * synchronously during scroll. + */ + internal var remeasurement: Remeasurement? = null + private set + + /** The modifier which provides [remeasurement]. */ + internal val remeasurementModifier = + object : RemeasurementModifier { + override fun onRemeasurementAvailable(remeasurement: Remeasurement) { + this@ListState.remeasurement = remeasurement + } + } + + /** + * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is + * ready. + */ + internal val awaitLayoutModifier = AwaitFirstLayoutModifier() + + /** + * The index of the first item that is visible within the scrollable viewport area not including + * items in the content padding region. For the first visible item that includes items in the + * content padding please use [ListLayoutInfo.visibleItemsInfo]. + * + * Note that this property is observable and if you use it in the composable function it will be + * recomposed on every change causing potential performance issues. + */ + public val firstVisibleItemIndex: Int + @FrequentlyChangingValue get() = scrollPosition.index + + /** + * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount + * that the item is offset backwards. + * + * Note that this property is observable and if you use it in the composable function it will be + * recomposed on every scroll causing potential performance issues. + */ + public val firstVisibleItemScrollOffset: Int + @FrequentlyChangingValue get() = scrollPosition.scrollOffset + + /** + * The object of [ListLayoutInfo] calculated during the last layout pass. For example, you can + * use it to calculate what items are currently visible. + * + * Note that this property is observable and is updated after every scroll or remeasure. If you + * use it in the composable function it will be recomposed on every change causing potential + * performance issues including infinity recomposition loop. Therefore, avoid using it in the + * composition. + * + * If you want to run some side effects like sending an analytics event or updating a state + * based on this value consider using "snapshotFlow": + */ + public val layoutInfo: ListLayoutInfo + @FrequentlyChangingValue get() = layoutInfoState.value + + /** + * [InteractionSource] that will be used to dispatch drag events when this list is being + * dragged. If you want to know whether the fling (or animated scroll) is in progress, use + * [isScrollInProgress]. + */ + public val interactionSource: InteractionSource + get() = internalInteractionSource + + /** Snaps to the requested scroll position. */ + internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) { + scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) + remeasurement?.forceRemeasure() + } + + /** + * When the user provided custom keys for the items we can try to detect when there were items + * added or removed before our current first visible item and keep this item as the first + * visible one even given that its index has been changed. + */ + internal fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: GlimmerListItemProvider, + firstItemIndex: Int, + ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) + + /** + * Called during the measurement pass once the dispatched [incomingScroll] has been handled and + * the new item positions are known. + * + * @param result lazy list measuring results. + * @param consumedScroll defines how much scroll was consumed during the measure pass. + * @param scrollToCarryOver tracks the amount of scrolling that the internal logic has reported + * as consumed, but wants to save for later measurement. Also, if the list consumes more + * scroll than it was given (for example, due to rounding errors), this value can be set to a + * negative amount to balance it out in the next pass. This value should never be larger than + * [consumedScroll]. + */ + internal fun applyMeasureResult( + result: GlimmerListMeasureResult, + consumedScroll: Float, + scrollToCarryOver: Float, + ) { + this.consumedScroll = consumedScroll + this.carryOverScroll = scrollToCarryOver + + canScrollBackward = result.canScrollBackward + canScrollForward = result.canScrollForward + layoutInfoState.value = result + + scrollPosition.updateFromMeasureResult(result) + } + + internal fun onScroll(distance: Float): Float { + if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { + return 0f + } + + // Fast path. Skip measure pass. + if (abs(distance + carryOverScroll) <= 0.5f) { + // Inside measuring we do `scrollToBeConsumed.roundToInt()` so there will be no scroll + // if we have less than 0.5 pixels. So just accumulate it for the next pass. + carryOverScroll += distance + return distance + } + + incomingScroll = distance + // The `forceRemeasure()` invocation triggers the measure pass where [incomingScroll] + // will be used to update [consumedScroll] and [carryOverScroll] values. + remeasurement?.forceRemeasure() + // It's important to reset this value because there are measure passes + // triggered from outside scrolling. They read this value as well. + // So, after we used it, we need to reset it to zero. + incomingScroll = 0f + + return consumedScroll + } + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + awaitLayoutModifier.waitForFirstLayout() + backingState.scroll(scrollPriority, block) + } + + override fun dispatchRawDelta(delta: Float): Float = backingState.dispatchRawDelta(delta) + + override val isScrollInProgress: Boolean + get() = backingState.isScrollInProgress + + @get:Suppress("GetterSetterNames") + override var canScrollForward: Boolean by mutableStateOf(false) + private set + + @get:Suppress("GetterSetterNames") + override var canScrollBackward: Boolean by mutableStateOf(false) + private set + + override val scrollIndicatorState: ScrollIndicatorState + get() = _scrollIndicatorState + + private val _scrollIndicatorState = + object : ScrollIndicatorState { + override val scrollOffset: Int + @FrequentlyChangingValue + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + this@ListState.firstVisibleItemIndex * visibleItemsAverageSize + + this@ListState.firstVisibleItemScrollOffset + } + } + + override val contentSize: Int + @FrequentlyChangingValue + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + // Approximate size of all content + (totalItemsCount * visibleItemsAverageSize) - + // Subtract the final trailing spacing that is not shown + (if (totalItemsCount > 0) mainAxisItemSpacing else 0) + + // Add the inner paddings (which are separate from the item sizes) + beforeContentPadding + + afterContentPadding + } + } + + override val viewportSize: Int + get() = + with(layoutInfoState.value) { + if (this === EmptyLazyListMeasureResult) { + Int.MAX_VALUE + } else { + mainAxisViewportSize + } + } + } + + /** + * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset] + * pixels. + * + * @param index the index to which to scroll. Must be non-negative. + * @param scrollOffset the offset that the item should end up after the scroll. Note that + * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will + * scroll the item further upward (taking it partly offscreen). + */ + public suspend fun scrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { + scroll { snapToItemIndexInternal(index, scrollOffset) } + } + + /** + * Animate (smooth scroll) to the given item. + * + * @param index the index to which to scroll. Must be non-negative. + * @param scrollOffset the offset that the item should end up after the scroll. Note that + * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will + * scroll the item further upward (taking it partly offscreen). + */ + public suspend fun animateScrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { + scroll { + GlimmerListScrollScope(this@ListState, this) + .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density) + } + } + + public companion object { + /** The default [Saver] implementation for [ListState]. */ + public val Saver: Saver = + listSaver( + save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, + restore = { + ListState(firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1]) + }, + ) + } +} + +private val EmptyLazyListMeasureResult = + GlimmerListMeasureResult( + firstVisibleItem = null, + firstVisibleItemScrollOffset = 0, + canScrollForward = false, + consumedScroll = 0f, + measureResult = + object : MeasureResult { + override val width: Int = 0 + override val height: Int = 0 + + @Suppress("PrimitiveInCollection") + override val alignmentLines: Map = emptyMap() + + override fun placeChildren() {} + }, + visibleItemsInfo = emptyList(), + viewportStartOffset = 0, + viewportEndOffset = 0, + totalItemsCount = 0, + reverseLayout = false, + orientation = Orientation.Vertical, + afterContentPadding = 0, + mainAxisItemSpacing = 0, + remeasureNeeded = false, + density = Density(1f), + childConstraints = Constraints(), + ) +``` + +
\ No newline at end of file From aaf42b970f9e6ee49e38aabd5cd0d00612e04a5a Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 10 Jul 2026 08:08:50 +0000 Subject: [PATCH 30/61] Updates skills (2026-07-10 08:08) --- camera/camerax/SKILL.md | 9 +- .../camerax/references/camera1-to-camerax.md | 198 +++++ .../camerax/references/camera2-to-camerax.md | 193 +++++ jetpack-compose/theming/styles/SKILL.md | 2 +- navigation/navigation-3/SKILL.md | 4 +- .../navigation-3/migration-guide.md | 14 +- .../navigation-3/recipes/deeplinks-basic.md | 744 ------------------ .../navigation-3/recipes/results-event.md | 15 +- .../navigation-3/recipes/results-state.md | 16 +- play/engage-sdk-integration/SKILL.md | 2 +- .../playcore/engage/tv/getting-started.md | 4 +- wear/wear-compose-m3/SKILL.md | 53 +- .../SKILL.md | 2 +- .../references/deptheffect-source.md | 11 +- .../references/glimmertheme-source.md | 9 +- .../references/listitem-source.md | 9 +- 16 files changed, 480 insertions(+), 805 deletions(-) create mode 100644 camera/camerax/references/camera1-to-camerax.md create mode 100644 camera/camerax/references/camera2-to-camerax.md delete mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md diff --git a/camera/camerax/SKILL.md b/camera/camerax/SKILL.md index e6eebe1..6ae6ca1 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-07-02' + last-updated: '2026-07-07' keywords: - recipe - Android @@ -68,6 +68,13 @@ run { See [immutability](references/immutability.md) for a list of affected classes. +### Migrating to CameraX + +When migrating legacy camera codebases to the CameraX Jetpack library: + +- **Camera1 to CameraX** : For migrating legacy `android.hardware.Camera` implementations, surface handling, and manual lifecycles, see the [Camera1 migration guide](references/camera1-to-camerax.md). +- **Camera2 to CameraX** : For migrating more recent but verbose `android.hardware.camera2` implementations, session state callbacks, and interop patterns, see the [Camera2 migration guide](references/camera2-to-camerax.md). + ### Comprehensive feature blueprinting For multi-step features that involve multiple files and hardware-level wiring, diff --git a/camera/camerax/references/camera1-to-camerax.md b/camera/camerax/references/camera1-to-camerax.md new file mode 100644 index 0000000..9c6e80e --- /dev/null +++ b/camera/camerax/references/camera1-to-camerax.md @@ -0,0 +1,198 @@ +## Remove `Camera1` implementation + +1. Delete all `android.hardware.Camera` instances. +2. Delete `SurfaceView` and `SurfaceHolder.Callback` implementations `surfaceCreated`, `surfaceChanged`, and `surfaceDestroyed`. +3. Remove custom lifecycle handling that opens or releases the camera in `onResume` or `onPause`. +4. Remove manual matrix calculations for orientation. + +## Initialize `ProcessCameraProvider` + +Request the `ProcessCameraProvider` and bind use cases to the Activity or +Fragment lifecycle. + + +```kotlin +val context = LocalContext.current +val lifecycleOwner = LocalLifecycleOwner.current +LaunchedEffect(context, lifecycleOwner) { + val cameraProvider = ProcessCameraProvider.getInstance(context).await() + + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + + val preview = Preview.Builder().build() + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + cameraProvider.unbindAll() // Unbind before rebinding + + val camera = cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture + ) + val cameraControl = camera.cameraControl +} +``` + +
+ +## Implement the preview and tap-to-focus + +Choose exactly one of the following patterns based on the app's UI toolkit: + +### Option A: For Android Views + +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) + ``` + +
+ +### Option B: For Jetpack Compose + +Use `androidx.camera.compose.CameraXViewfinder`. + +1. **Set up preview and SurfaceRequest**: + + + ```kotlin + var surfaceRequest by remember { mutableStateOf(null) } + val preview = remember { + Preview.Builder().build().apply { + setSurfaceProvider { request -> surfaceRequest = request } + } + } + ``` + +
+ +2. **Render viewfinder**: + + + ```kotlin + surfaceRequest?.let { request -> + CameraXViewfinder( + surfaceRequest = request, + coordinateTransformer = coordinateTransformer, + modifier = Modifier + ) + } + ``` + +
+ +3. **Handle tap-to-focus in Compose**: + + + ```kotlin + // Inside your tap gesture handler... + val surfaceCoords = with(coordinateTransformer) { offset.transform() } + val factory = SurfaceOrientedMeteringPointFactory( + request.resolution.width.toFloat(), + request.resolution.height.toFloat() + ) + 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) { + if (!view.isInEditMode) { + val rotation = view.display?.rotation ?: Surface.ROTATION_0 + imageCapture.targetRotation = rotation + preview.targetRotation = rotation + } + } + ``` + +
+ +## Capture a photo + +Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles +rotation directly. + + +```kotlin +imageCapture.takePicture( + cameraExecutor, + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + + // Adjust rotation natively via ImageProxy + val matrix = Matrix() + matrix.postRotate(image.imageInfo.rotationDegrees.toFloat()) + if (lensFacing == CameraSelector.LENS_FACING_FRONT) { + matrix.postScale(-1f, 1f) // Mirror for front camera + } + + val rotatedBitmap = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true + ) + + // MUST close proxy + image.close() + } + + override fun onError(exception: ImageCaptureException) { + Log.e("CameraX", "Capture failed: ${exception.message}", exception) + } + } +) +``` + +
+ +## Switch cameras + +To flip between front and rear cameras, change the `CameraSelector` and +retrigger the `ProcessCameraProvider` logic. + + +```kotlin +lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) { + CameraSelector.LENS_FACING_FRONT +} else { + CameraSelector.LENS_FACING_BACK +} +``` + +
+ +## Follow constraints + +- **Don't manage the camera lifecycle manually** : Bind the camera to a `LifecycleOwner` through the `ProcessCameraProvider`. Avoid manual camera open or close logic in `onResume` or `onPause`. +- **Don't calculate focus matrices manually** : `MeteringPointFactory` handles coordinate transformations, including device rotation offsets. Avoid custom matrix implementations. +- **Don't forget to close the `ImageProxy`** : Remember to invoke `image.close()` in the capture callback. Skipping this call locks the capture pipeline and interrupts subsequent photos. +- **Don't wrap `PreviewView` in `AndroidView` for Compose code** : For Compose UI layouts, use `CameraXViewfinder`. Compiling `PreviewView` in an `AndroidView` is an earlier fallback option that introduces resizing issues. \ No newline at end of file diff --git a/camera/camerax/references/camera2-to-camerax.md b/camera/camerax/references/camera2-to-camerax.md new file mode 100644 index 0000000..5b21f87 --- /dev/null +++ b/camera/camerax/references/camera2-to-camerax.md @@ -0,0 +1,193 @@ +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. + +*** ** * ** *** + +## Remove Camera2 boilerplate + +Migrating to CameraX removes manual setup code: + +1. **Delete manual thread handling** : Remove `HandlerThread`, `Handler`, and executors dedicated to camera background tasks. CameraX manages its own threads. +2. **Remove session and device callbacks** : Delete `CameraDevice.StateCallback` and `CameraCaptureSession.StateCallback` implementations. +3. **Remove surface management** : Remove manual routing of `Surface` objects from `TextureView` or `SurfaceView` to the camera device. +4. **Remove manual orientation logic** : Delete calculations involving `CameraCharacteristics.SENSOR_ORIENTATION` and display rotation for adjusting preview and capture orientation. + +*** ** * ** *** + +## Initialize `ProcessCameraProvider` + +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 +val lifecycleOwner = LocalLifecycleOwner.current +LaunchedEffect(context, lifecycleOwner) { + val cameraProvider = ProcessCameraProvider.getInstance(context).await() + + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + + val preview = Preview.Builder().build() + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + // ImageAnalysis is common when migrating from Camera2 ImageReader + val imageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + cameraProvider.unbindAll() + + val camera = cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageCapture, + imageAnalysis + ) +} +``` + +
+ +*** ** * ** *** + +## Implement the preview and tap-to-focus + +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. + + +```kotlin +preview.setSurfaceProvider(previewView.surfaceProvider) +``` + +
+ +### Option B: For Jetpack Compose + +Use `androidx.camera.compose.CameraXViewfinder`. + + +```kotlin +var surfaceRequest by remember { mutableStateOf(null) } +val preview = remember { + Preview.Builder().build().apply { + setSurfaceProvider { request -> surfaceRequest = request } + } +} +``` + +
+ +*** ** * ** *** + +## 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`. + + +```kotlin +imageCapture.takePicture( + cameraExecutor, + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + if (bitmap != null) { + val matrix = Matrix() + matrix.postRotate(image.imageInfo.rotationDegrees.toFloat()) + if (lensFacing == CameraSelector.LENS_FACING_FRONT) { + matrix.postScale(-1f, 1f) + } + + val rotatedBitmap = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true + ) + } + + // MUST close proxy + image.close() + } + + override fun onError(exception: ImageCaptureException) { + Log.e("CameraX", "Capture failed: ${exception.message}", exception) + } + } +) +``` + +
+ +*** ** * ** *** + +## 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. + + +```kotlin +imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> + try { + val rotationDegrees = imageProxy.imageInfo.rotationDegrees + // Process image here (e.g., run object detection) + // ... + } finally { + // MUST close the imageProxy to avoid blocking the pipeline + imageProxy.close() + } +} +``` + +
+ +*** ** * ** *** + +## 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. + + +```kotlin +// Use Camera2Interop to set Camera2-specific capture options +val extender = Camera2Interop.Extender(imageCaptureBuilder) +extender.setCaptureRequestOption( + CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_OFF +).setCaptureRequestOption( + CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH +) +``` + +
+ +*** ** * ** *** + +## Follow constraints + +- **Don't open the CameraDevice manually** : Let CameraX manage device opening and closing using `bindToLifecycle`. +- **Don't forget to close `ImageProxy`** : In both `ImageCapture` and `ImageAnalysis` callbacks, you **must** call `image.close()`. Failure to do so will block the camera pipeline. +- **Don't block the ImageAnalysis thread** : The analyzer runs frame-by-frame. If you need to perform heavy computations, offload them to a background worker and close the `ImageProxy` as soon as possible. \ No newline at end of file diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index daf7256..c223382 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-07-02' + last-updated: '2026-07-07' keywords: - Jetpack Compose - Styles diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index 3704be5..4e57942 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-07-02' + last-updated: '2026-07-09' keywords: - recipe - Android @@ -63,7 +63,7 @@ Code examples showcasing common patterns. ### Deep links -- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. +- *[Basic](https://developer.android.com/guide/navigation/navigation-3/recipes/deeplinks-basic)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. - *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior. ### Scenes 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 ba86f3c..a7dfe7e 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 @@ -438,12 +438,12 @@ For example: ScreenA(title = "Screen has ID: $id") } featureBSection() - dialog{ ScreenD() } + < di>alogRouteD{ ScreenD() } } fun NavGraphBuilder.featureBSection() { - navigation(startDestination = RouteB) { - composable { ScreenB() } + BaseRouteB(startDestination = RouteB) { + ableRouteB { ScreenB() } } } @@ -460,12 +460,12 @@ becomes: val entryProvider = entryProvider { entry{ key -> ScreenA(title = "Screen has ID: ${key.id}") } - featureBSection() - entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } + featureBSection()< + e>ntryRouteD(metadata = DialogSceneStrategy.dialog()){ ScreenD() } } - fun EntryProviderScope.featureBSection() { - entry { ScreenB() } + fun EntryProcopeNavKey.featureBSection() {< + e>ntryRouteB { ScreenB() } } ## Step 6: Replace `NavHost` with `NavDisplay` diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md deleted file mode 100644 index 66312de..0000000 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md +++ /dev/null @@ -1,744 +0,0 @@ -# Deep Link Basic Recipe - -This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. - -## How it works - -It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. - -## Demonstrated forms of deeplink - -The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: - -1. `HomeKey` - deeplink with an exact url (no deeplink arguments) -2. `UsersKey` - deeplink with path arguments -3. `SearchKey` - deeplink with query arguments - -See `MainActivity.deepLinkPatterns` for the actual url pattern of each. - -## Recipe structure - -This recipe consists of three main packages: - -1. `basic.deeplink` - Contains the two activities -2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc -3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks - -[![](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/basic) - -``` -package com.example.nav3recipes.deeplink.basic - -import androidx.navigation3.runtime.NavKey -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS -import kotlinx.serialization.Serializable - -internal interface NavRecipeKey: NavKey { - val name: String -} - -@Serializable -internal object HomeKey: NavRecipeKey { - override val name: String = STRING_LITERAL_HOME -} - -@Serializable -internal data class UsersKey( - val filter: String, -): NavRecipeKey { - override val name: String = STRING_LITERAL_USERS - companion object { - const val FILTER_KEY = STRING_LITERAL_FILTER - const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" - const val FILTER_OPTION_ALL = "all" - } -} - -@Serializable -internal data class SearchKey( - val firstName: String? = null, - val ageMin: Int? = null, - val ageMax: Int? = null, - val location: String? = null, -): NavRecipeKey { - override val name: String = STRING_LITERAL_SEARCH -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.core.net.toUri -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FriendsList -import com.example.nav3recipes.common.deeplink.LIST_USERS -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT -import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher -import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern -import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest -import com.example.nav3recipes.deeplink.basic.util.KeyDecoder -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * Parses a target deeplink into a NavKey. There are several crucial steps involved: - * - * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable - * format (see [DeepLinkPattern]) - * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) - * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each - * other to facilitate comparison and finding a match - * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match - * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result - * to backstack key, regardless of what the conversion method may be. - * STEP 4. Associate the match results with the correct backstack key - * - * This recipes provides an example for each of the above steps by way of kotlinx.serialization. - * - * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional - * deeplink considerations are not included in this scope** - * - Create synthetic backStack - * - Multi-modular setup - * - DI - * - Managing TaskStack - * - Up button ves Back Button - * - */ -class MainActivity : ComponentActivity() { - /** STEP 1. Parse supported deeplinks */ - // internal so that landing activity can link to this in the kdocs - internal val deepLinkPatterns: List> = listOf( - // "https://www.nav3recipes.com/home" - DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), - // "https://www.nav3recipes.com/users/with/{filter}" - DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), - // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" - DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), - ) - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - // retrieve the target Uri - val uri: Uri? = intent.data - // associate the target with the correct backstack key - val key: NavKey = uri?.let { - /** STEP 2. Parse requested deeplink */ - val request = DeepLinkRequest(uri) - /** STEP 3. Compared requested with supported deeplink to find match*/ - val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> - DeepLinkMatcher(request, pattern).match() - } - /** STEP 4. If match is found, associate match to the correct key*/ - match?.let { - //leverage kotlinx.serialization's Decoder to decode - // match result into a backstack key - KeyDecoder(match.args) - .decodeSerializableValue(match.serializer) - } - } ?: HomeKey // fallback if intent.uri is null or match is not found - - /** - * Then pass starting key to backstack - */ - setContent { - val backStack: NavBackStack = rememberNavBackStack(key) - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { key -> - EntryScreen(key.name) { - TextContent("") - } - } - entry { key -> - EntryScreen("${key.name} : ${key.filter}") { - TextContent("") - val list = when { - key.filter.isEmpty() -> LIST_USERS - key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS - else -> LIST_USERS.take(5) - } - FriendsList(list) - } - } - entry { search -> - EntryScreen(search.name) { - TextContent("") - val matchingUsers = LIST_USERS.filter { user -> - (search.firstName == null || user.firstName == search.firstName) && - (search.location == null || user.location == search.location) && - (search.ageMin == null || user.age >= search.ageMin) && - (search.ageMax == null || user.age <= search.ageMax) - } - FriendsList(matchingUsers) - } - } - } - ) - } - } -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.core.net.toUri -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.EMPTY -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE -import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY -import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM -import com.example.nav3recipes.common.deeplink.LOCATION_BC -import com.example.nav3recipes.common.deeplink.LOCATION_BR -import com.example.nav3recipes.common.deeplink.LOCATION_CA -import com.example.nav3recipes.common.deeplink.LOCATION_US -import com.example.nav3recipes.common.deeplink.MenuDropDown -import com.example.nav3recipes.common.deeplink.MenuTextInput -import com.example.nav3recipes.common.deeplink.PaddedButton -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE -import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE -import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * This activity allows the user to create a deep link and make a request with it. - * - * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct - * and trigger the deeplink request, and the [MainActivity] to show how an app can handle - * that request. - * - * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to - * demonstrate different types of supported deeplinks: - * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) - * 2. [UsersKey] - deeplink with path arguments - * 3. [SearchKey] - deeplink with query arguments - * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. - * - * **RECIPE STRUCTURE** This recipe consists of three main packages: - * 1. basic.deeplink - Contains the two activities - * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc - * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match - * the deeplinks - * - * See [MainActivity] for how the requested deeplink is handled. - */ -class CreateDeepLinkActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - /** - * UI for deeplink sandbox - */ - EntryScreen("Sandbox - Build Your Deeplink") { - TextContent("Base url:\n${PATH_BASE}/") - var showFilterOptions by remember { mutableStateOf(false) } - val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } - - var showQueryOptions by remember { mutableStateOf(false) } - var selectedFilter by remember { mutableStateOf("") } - val selectedSearchQuery = remember { mutableStateMapOf() } - - // manage path options - MenuDropDown( - menuOptions = MENU_OPTIONS_PATH, - ) { _, selection -> - selectedPath.value = selection - when (selection) { - PATH_SEARCH -> { - showQueryOptions = true - showFilterOptions = false - } - - PATH_INCLUDE -> { - showQueryOptions = false - showFilterOptions = true - } - - else -> { - showQueryOptions = false - showFilterOptions = false - } - } - } - - // manage path filter options, reset state if menu is closed - LaunchedEffect(showFilterOptions) { - selectedFilter = if (showFilterOptions) { - MENU_OPTIONS_FILTER.values.first().first() - } else { - "" - } - } - if (showFilterOptions) { - MenuDropDown( - menuOptions = MENU_OPTIONS_FILTER, - ) { _, selected -> - selectedFilter = selected - } - } - - // manage query options, reset state if menu is closed - LaunchedEffect(showQueryOptions) { - if (showQueryOptions) { - val initEntry = MENU_OPTIONS_SEARCH.entries.first() - selectedSearchQuery[initEntry.key] = initEntry.value.first() - } else { - selectedSearchQuery.clear() - } - } - if (showQueryOptions) { - MenuTextInput( - menuLabels = MENU_LABELS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - MenuDropDown( - menuOptions = MENU_OPTIONS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - } - - // form final deeplink url - val arguments = when (selectedPath.value) { - PATH_INCLUDE -> "/${selectedFilter}" - PATH_SEARCH -> { - buildString { - selectedSearchQuery.forEach { entry -> - if (entry.value.isNotEmpty()) { - val prefix = if (isEmpty()) "?" else "&" - append("$prefix${entry.key}=${entry.value}") - } - } - } - } - - else -> "" - } - val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" - TextContent("Final url:\n$finalUrl") - // deeplink to target - PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { - val intent = Intent( - this@CreateDeepLinkActivity, - MainActivity::class.java - ) - // start activity with the url - intent.data = finalUrl.toUri() - startActivity(intent) - }) - } - } - } -} - -private const val KEY_PATH = "path" -private val MENU_OPTIONS_PATH = mapOf( - KEY_PATH to listOf( - STRING_LITERAL_HOME, - PATH_INCLUDE, - PATH_SEARCH, - ), -) - -private val MENU_OPTIONS_FILTER = mapOf( - UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), -) - -private val MENU_OPTIONS_SEARCH = mapOf( - SearchKey::firstName.name to listOf( - EMPTY, - FIRST_NAME_JOHN, - FIRST_NAME_TOM, - FIRST_NAME_MARY, - FIRST_NAME_JULIE - ), - SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) -) - -private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) - -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -/** - * Decodes the list of arguments into a a back stack key - * - * **IMPORTANT** This decoder assumes that all argument types are Primitives. - */ -@OptIn(ExperimentalSerializationApi::class) -internal class KeyDecoder( - private val arguments: Map, -) : AbstractDecoder() { - - override val serializersModule: SerializersModule = EmptySerializersModule() - private var elementIndex: Int = -1 - private var elementName: String = "" - - /** - * Decodes the index of the next element to be decoded. Index represents a position of the - * current element in the [descriptor] that can be found with [descriptor].getElementIndex. - * - * The returned index will trigger deserializer to call [decodeValue] on the argument at that - * index. - * - * The decoder continually calls this method to process the next available argument until this - * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more - * arguments to decode. - * - * This method should sequentially return the element index for every element that has its value - * available within [arguments]. - */ - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - var currentIndex = elementIndex - while (true) { - // proceed to next element - currentIndex++ - // if we have reached the end, let decoder know there are not more arguments to decode - if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE - val currentName = descriptor.getElementName(currentIndex) - // Check if bundle has argument value. If so, we tell decoder to process - // currentIndex. Otherwise, we skip this index and proceed to next index. - if (arguments.contains(currentName)) { - elementIndex = currentIndex - elementName = currentName - return elementIndex - } - } - } - - /** - * Returns argument value from the [arguments] for the argument at the index returned by - * [decodeElementIndex] - */ - override fun decodeValue(): Any { - val arg = arguments[elementName] - checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } - return arg - } - - override fun decodeNull(): Nothing? = null - - // we want to know if it is not null, so its !isNull - override fun decodeNotNullMark(): Boolean = arguments[elementName] != null -} -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri - -/** - * Parse the requested Uri and store it in a easily readable format - * - * @param uri the target deeplink uri to link to - */ -internal class DeepLinkRequest( - val uri: Uri -) { - /** - * A list of path segments - */ - val pathSegments: List = uri.pathSegments - - /** - * A map of query name to query value - */ - val queries = buildMap { - uri.queryParameterNames.forEach { argName -> - this[argName] = uri.getQueryParameter(argName)!! - } - } - - // TODO add parsing for other Uri components, i.e. fragments, mimeType, action -} -``` - -```` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.encoding.CompositeDecoder -import java.io.Serializable - -/** - * Parse a supported deeplink and stores its metadata as a easily readable format - * - * The following notes applies specifically to this particular sample implementation: - * - * The supported deeplink is expected to be built from a serializable backstack key [T] that - * supports deeplink. This means that if this deeplink contains any arguments (path or query), - * the argument name must match any of [T] member field name. - * - * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] - * supports two deeplink patterns: - * ``` - * val deeplink1 = www.nav3recipes.com/home - * val deeplink2 = www.nav3recipes.com/profile/{userId} - * ``` - * Then two [DeepLinkPattern] should be created - * ``` - * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) - * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) - * ``` - * - * This implementation assumes a few things: - * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match - * 2. all query arguments are optional by way of nullable/has default value - * - * @param T the backstack key type that supports the deeplinking of [uriPattern] - * @param serializer the serializer of [T] - * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" - */ -internal class DeepLinkPattern( - val serializer: KSerializer, - val uriPattern: Uri -) { - /** - * Help differentiate if a path segment is an argument or a static value - */ - private val regexPatternFillIn = Regex("\\{(.+?)\\}") - - // TODO make these lazy - /** - * parse the path into a list of [PathSegment] - * - * order matters here - path segments need to match in value and order when matching - * requested deeplink to supported deeplink - */ - val pathSegments: List = buildList { - uriPattern.pathSegments.forEach { segment -> - // first, check if it is a path arg - var result = regexPatternFillIn.find(segment) - if (result != null) { - // if so, extract the path arg name (the string value within the curly braces) - val argName = result.groups[1]!!.value - // from [T], read the primitive type of this argument to get the correct type parser - val elementIndex = serializer.descriptor.getElementIndex(argName) - if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { - throw IllegalArgumentException( - "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." - ) - } - - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - // finally, add the arg name and its respective type parser to the map - add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) - } else { - // if its not a path arg, then its just a static string path segment - add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) - } - } - } - - /** - * Parse supported queries into a map of queryParameterNames to [TypeParser] - * - * This will be used later on to parse a provided query value into the correct KType - */ - val queryValueParsers: Map = buildMap { - uriPattern.queryParameterNames.forEach { paramName -> - val elementIndex = serializer.descriptor.getElementIndex(paramName) - // Ignore static query parameters that are not in the Serializable class - if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - this[paramName] = getTypeParser(elementDescriptor.kind) - } - } - } - - /** - * Metadata about a supported path segment - */ - class PathSegment( - val stringValue: String, - val isParamArg: Boolean, - val typeParser: TypeParser - ) -} - -/** - * Parses a String into a Serializable Primitive - */ -private typealias TypeParser = (String) -> Serializable - -private fun getTypeParser(kind: SerialKind): TypeParser { - return when (kind) { - PrimitiveKind.STRING -> Any::toString - PrimitiveKind.INT -> String::toInt - PrimitiveKind.BOOLEAN -> String::toBoolean - PrimitiveKind.BYTE -> String::toByte - PrimitiveKind.CHAR -> String::toCharArray - PrimitiveKind.DOUBLE -> String::toDouble - PrimitiveKind.FLOAT -> String::toFloat - PrimitiveKind.LONG -> String::toLong - PrimitiveKind.SHORT -> String::toShort - else -> throw IllegalArgumentException( - "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." - ) - } -} -```` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.util.Log -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer - -internal class DeepLinkMatcher( - val request: DeepLinkRequest, - val deepLinkPattern: DeepLinkPattern -) { - /** - * Match a [DeepLinkRequest] to a [DeepLinkPattern]. - * - * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise - */ - fun match(): DeepLinkMatchResult? { - if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null - if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null - if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null - // exact match (url does not contain any arguments) - if (request.uri == deepLinkPattern.uriPattern) - return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) - - val args = mutableMapOf() - // match the path - request.pathSegments - .asSequence() - // zip to compare the two objects side by side, order matters here so we - // need to make sure the compared segments are at the same position within the url - .zip(deepLinkPattern.pathSegments.asSequence()) - .forEach { it -> - // retrieve the two path segments to compare - val requestedSegment = it.first - val candidateSegment = it.second - // if the potential match expects a path arg for this segment, try to parse the - // requested segment into the expected type - if (candidateSegment.isParamArg) { - val parsedValue = try { - candidateSegment.typeParser.invoke(requestedSegment) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) - return null - } - args[candidateSegment.stringValue] = parsedValue - } else if(requestedSegment != candidateSegment.stringValue){ - // if it's path arg is not the expected type, its not a match - return null - } - } - // match queries (if any) - request.queries.forEach { query -> - val name = query.key - // If the pattern does not define this query parameter, ignore it. - // This prevents a NullPointerException. - val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach - - val queryParsedValue = try { - queryStringParser.invoke(query.value) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) - return null - } - args[name] = queryParsedValue - } - // provide the serializer of the matching key and map of arg names to parsed arg values - return DeepLinkMatchResult(deepLinkPattern.serializer, args) - } -} - - -/** - * Created when a requested deeplink matches with a supported deeplink - * - * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink - * @param serializer serializer for [T] - * @param args The map of argument name to argument value. The value is expected to have already - * been parsed from the raw url string back into its proper KType as declared in [T]. - * Includes arguments for all parts of the uri - path, query, etc. - * */ -internal data class DeepLinkMatchResult( - val serializer: KSerializer, - val args: Map -) - -const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" -``` - -``` -package com.example.nav3recipes.deeplink.basic.ui - -import com.example.nav3recipes.deeplink.basic.SearchKey - -/** - * String resources - */ -internal const val STRING_LITERAL_FILTER = "filter" -internal const val STRING_LITERAL_HOME = "home" -internal const val STRING_LITERAL_USERS = "users" -internal const val STRING_LITERAL_SEARCH = "search" -internal const val STRING_LITERAL_INCLUDE = "include" -internal const val PATH_BASE = "https://www.nav3recipes.com" -internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" -internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" -internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" - -internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" -internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + - "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + - "&${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/results-event.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md index a61736f..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 @@ -91,11 +91,10 @@ class PersonDetailsForm : NavKey package com.example.nav3recipes.results.common -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelable +@Serializable +data class Person(val name: String, val favoriteColor: String) ``` ``` @@ -207,14 +206,13 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.result.LocalResultEventBus import androidx.navigation3.runtime.result.ResultEffect -import androidx.navigation3.runtime.result.ResultEventBus import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.results.common.Home @@ -240,7 +238,10 @@ class ResultEventActivity : ComponentActivity() { backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, - entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberResultEventBusNavEntryDecorator() + ), entryProvider = entryProvider { entry { val viewModel = viewModel(key = Home.toString()) 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 71c2ccf..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 @@ -91,11 +91,10 @@ class PersonDetailsForm : NavKey package com.example.nav3recipes.results.common -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -data class Person(val name: String, val favoriteColor: String) : Parcelable +@Serializable +data class Person(val name: String, val favoriteColor: String) ``` ``` @@ -208,16 +207,14 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.result.LocalResultEventBus -import androidx.navigation3.runtime.result.ResultEffect import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.results.common.Home import com.example.nav3recipes.results.common.HomeScreen -import com.example.nav3recipes.results.common.HomeViewModel import com.example.nav3recipes.results.common.Person import com.example.nav3recipes.results.common.PersonDetailsForm import com.example.nav3recipes.results.common.PersonDetailsScreen @@ -236,7 +233,10 @@ class ResultStateActivity : ComponentActivity() { backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, - entryDecorators = listOf(rememberResultEventBusNavEntryDecorator()), + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberResultEventBusNavEntryDecorator() + ), entryProvider = entryProvider { entry { val resultState = LocalResultEventBus diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index afdb402..9927e22 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-07-02' + last-updated: '2026-07-09' keywords: - android - engage 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 9218a8a..49d8975 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 @@ -168,6 +168,6 @@ This is the Android Software Development Kit License Agreement ### 14. General Legal Terms -14.1 The License Agreement constitutes the whole legal agreement between you and Google and governs your use of the SDK (excluding any services which Google may provide to you under a separate written agreement), and completely replaces any prior agreements between you and Google in relation to the SDK. 14.2 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the License Agreement (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google. 14.3 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of the License Agreement is invalid, then that provision will be removed from the License Agreement without affecting the rest of the License Agreement. The remaining provisions of the License Agreement will continue to be valid and enforceable. 14.4 You acknowledge and agree that each member of the group of companies of which Google is the parent will be third party beneficiaries to the License Agreement and that such other companies will be entitled to directly enforce, and rely upon, any provision of the License Agreement that confers a benefit on (or rights in favor of) them. Other than this, no other person or company will be third party beneficiaries to the License Agreement. 14.5 EXPORT RESTRICTIONS. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON DESTINATIONS, END USERS AND END USE. 14.6 The rights granted in the License Agreement may not be assigned or transferred by either you or Google without the prior written approval of the other party. Neither you nor Google will be permitted to delegate their responsibilities or obligations under the License Agreement without the prior written approval of the other party. 14.7 The License Agreement, and your relationship with Google under the License Agreement, will be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google will still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction. *April 28, 2026* I have read and agree with the above terms and conditions [Download](https://dl.google.com/developers/android/channels/verify_app_multiplatform_public_20251215.apk) +14.1 The License Agreement constitutes the whole legal agreement between you and Google and governs your use of the SDK (excluding any services which Google may provide to you under a separate written agreement), and completely replaces any prior agreements between you and Google in relation to the SDK. 14.2 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the License Agreement (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google. 14.3 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of the License Agreement is invalid, then that provision will be removed from the License Agreement without affecting the rest of the License Agreement. The remaining provisions of the License Agreement will continue to be valid and enforceable. 14.4 You acknowledge and agree that each member of the group of companies of which Google is the parent will be third party beneficiaries to the License Agreement and that such other companies will be entitled to directly enforce, and rely upon, any provision of the License Agreement that confers a benefit on (or rights in favor of) them. Other than this, no other person or company will be third party beneficiaries to the License Agreement. 14.5 EXPORT RESTRICTIONS. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON DESTINATIONS, END USERS AND END USE. 14.6 The rights granted in the License Agreement may not be assigned or transferred by either you or Google without the prior written approval of the other party. Neither you nor Google will be permitted to delegate their responsibilities or obligations under the License Agreement without the prior written approval of the other party. 14.7 The License Agreement, and your relationship with Google under the License Agreement, will be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google will still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction. *April 28, 2026* I have read and agree with the above terms and conditions [Download](https://dl.google.com/developers/android/channels/verify_app_multiplatform_public_20260701.apk) -*verify_app_multiplatform_public_20251215.apk* \ No newline at end of file +*verify_app_multiplatform_public_20260701.apk* \ No newline at end of file diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md index 2cb5b96..a9618d6 100644 --- a/wear/wear-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -1,14 +1,15 @@ --- name: wear-compose-m3 description: Expert guidance for working with Wear OS Compose Material3. Use this - skill when creating, updating or migrating Wear OS projects. This includes the androidx.wear.compose.material3, - androidx.wear.compose.foundation and androidx.wear.compose.navigation3 libraries. - Also working with core components such as AppScaffold, ScreenScaffold and TransformingLazyColumn. - Migration from earlier versions such as Material 2.5 and Horologist. + skill when creating, updating, or migrating Wear OS projects. This includes the + androidx.wear.compose.material3, androidx.wear.compose.foundation, and androidx.wear.compose.navigation3 + libraries. Also working with core components such as AppScaffold, ScreenScaffold, + and TransformingLazyColumn, and core Wear OS concepts such as ambient mode. Migration + from lower versions such as Material 2.5 and Horologist. license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-02' + last-updated: '2026-07-08' keywords: - Wear OS - Compose @@ -21,16 +22,17 @@ metadata: ## Prerequisites and compatibility -1. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. +1. **Current Wear OS Compose version in-use:** To find the installed library version, read `gradle/libs.versions.toml` or `build.gradle.kts` directly. Don't run `./gradlew dependencies` or other shell commands to resolve versions. +2. **Wear OS Compose Material3 version:** If an internal tool is available to establish the **latest stable version** `{VERSION}` of `androidx.wear.compose:compose-material3`, use that tool. - Otherwise, fetch the [official Maven metadata XML](https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-material3/maven-metadata.xml) to identify `{VERSION}` (highest number, ignoring `-alpha`, `-beta`, or `-rc`). -2. **Strict compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. -3. **Kotlin version:** For Wear Compose Material3, use Kotlin **2.0.0 or +3. **Strict compliance:** If a version is listed as stable, you MUST use it, unless overridden by the user. Do not downgrade based on initial "Unresolved reference" errors in the editor or outdated web search results. +4. **Kotlin version:** For Wear Compose Material3, use Kotlin **2.0.0 or higher**. -4. **Compose compiler:** +5. **Compose compiler:** - If Kotlin version is **2.0.0+** , the project must use the `org.jetbrains.kotlin.plugin.compose` Gradle plugin. - If Kotlin version is **\< 2.0.0** , the project must use `kotlinCompilerExtensionVersion` in `composeOptions`, matching the [Compose to Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). -5. **Min SDK:** Ensure `minSdk` is at least **25** (Wear OS 2.0). -6. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You MUST NOT propose code changes until the samples in Capability 2 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure. +6. **Min SDK:** Ensure `minSdk` is at least **25**. +7. **Sample extraction mandate**: Wear Compose libraries ship with an additional JAR file which contains individual samples for each and every component. You mustn't propose code changes, other than previews or basic changes such as color changes, until the samples in Capability 3 are extracted to the local cache. Library source files are incomplete and NOT a substitute for these samples; bypassing extraction is an environment setup failure. ## Gotchas @@ -49,10 +51,21 @@ Horologist. 3. Use the official component mappings from the migration guide. 4. Before refactoring any component (for example, `Chip` -\> `Button`), check the parameter names, slot types, and "Expressive" design tokens. 5. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. -6. **Always** check against the component guidance in Capability 3. +6. **Always** check against the component guidance in Capability 4. 7. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. -### Capability 2: Component samples +### Capability 2: Adding Wear OS Compose Material3 features or updating the app + +Use this guidance when the developer asks to update a project which is using an +earlier version of Wear OS Compose Material3, or when they ask to add further +features. + +1. Unless otherwise indicated by the developer, use the latest stable version of Wear Compose Material3 from `{VERSION}`. +2. Do not use the Horologist Composables, Compose Layout, or Compose Material libraries. +3. **Always** check against the component guidance in Capability 4. +4. Expect screenshot tests to fail when a migration has been performed: Even when migrating to very similar components, expected defaults for padding and positioning will have changed. Do not seek to artificially match the pre-migration screenshot, but give preference to the Material3 defaults. + +### Capability 3: Component samples Wear Compose includes individual component samples for each and every component, within the `--samples-sources.jar` file. Gradle automatically @@ -68,6 +81,10 @@ with alternative search tools or by assuming library documentation is sufficient is a protocol breach. You MUST verify the local cache by reading a sample file before proceeding. +Exception: You don't need to extract samples if the request is strictly related +to tooling (for example, adding @WearPreviewDevices), updating text/colors, or +basic refactoring that doesn't involve adding new Wear Compose components. + #### Step 1: Prepare 1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. @@ -111,7 +128,7 @@ lookup. - Interactive behaviors (for example: `onClick`, `onLongClick`). - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). -### Capability 3: Component guidance +### Capability 4: Component guidance **Mandatory**: Use this capability as a checklist against any component use. It provides more holistic guidance on how to use each component in practice, beyond @@ -266,7 +283,7 @@ the component syntax. 8. Use component defaults: - \[ \] Components such as `Button` have a corresponding `ButtonDefaults` object. - - Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. + - \[ \] Check for and use the `*Defaults` object for any component when working with padding and styling values, in preference to hard-coded values. 9. Use Wear specific previews: - \[ \] `WearPreviewDevices` @@ -278,3 +295,9 @@ the component syntax. - \[ \] When adding navigation fresh, use Navigation3. - \[ \] For Navigation3 in Wear OS, use `SwipeDismissableSceneStrategy()` from the Wear Compose `compose-navigation3` library. +12. Comments + + - \[ \] Where any Kotlin file has been modified, ensure that the existing comments are up to date and accurately reflect any changes to the implementation. +13. `HorizontalPager` or `VerticalPager` + + - \[ \] Use the Composable hierarchy in this order: `AppScaffold`, `HorizontalPagerScaffold`, `HorizontalPager`, `AnimatedPage`, `ScreenScaffold`. Or similarly for `VerticalPager`. diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 63ce459..2a38ed0 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-02' + last-updated: '2026-07-09' keywords: - Jetpack Compose Glimmer - audio glasses diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md index db9d205..437fadd 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md @@ -108,8 +108,8 @@ public fun Modifier.depthEffect(depthEffect: DepthEffect?, shape: Shape): Modifi internal fun Modifier.depthEffect( from: DepthEffect?, to: DepthEffect?, - shape: Shape, - progress: () -> Float, + shap>e: Shape, + progress: () - Float, ): Modifier { // dropShadow draws the shadow, and then the content on top. So in order to get layer2 to // render on top of layer1, we draw layer1 first - this means that layer1's dropShadow will @@ -139,8 +139,8 @@ private fun ShadowScope.updateFrom(shadow: Shadow) { this.blendMode = shadow.blendMode } -private class DepthEffectElement(private val depthEffect: DepthEffect, private val shape: Shape) : - ModifierNodeElement() { +private class DepthEffectElement(private val depthEffect: DepthEffect, private< val shape: Sha>pe) : + ModifierNodeElementDepthEffectNode() { override fun create(): DepthEffectNode = DepthEffectNode(depthEffect, shape) @@ -216,9 +216,10 @@ internal class DepthEffectNode(private var depthEffect: DepthEffect, private var layer2ShadowPainter ?: requireGraphicsContext() .shadowContext - .createDropShadowPainter(shape, depthEffect.layer2) + .createDropShadowPainter(shape,ayer2) .also { layer2ShadowPainter = it } } +DepthEffect.kt ```
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md index 325f09a..19b4604 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md @@ -60,7 +60,7 @@ public fun GlimmerTheme( colors: Colors = GlimmerTheme.colors, typography: Typography = GlimmerTheme.typography, componentSpacingValues: ComponentSpacingValues = GlimmerTheme.componentSpacingValues, - content: @Composable () -> Unit, + conte>nt: @Composable () - Unit, ) { val theme = GlimmerTheme(colors, typography, componentSpacingValues) CompositionLocalProvider( @@ -136,7 +136,7 @@ public class GlimmerTheme( * retrieving values from inside CompositionLocalConsumerModifierNode implementations - in * most cases you should use [colors] and other properties directly. */ - public val LocalGlimmerTheme: CompositionLocal + pualGlimmerTheme: CompositionLocalGlimmerTheme get() = _localGlimmerTheme /** @@ -173,10 +173,11 @@ private object NoIndication : IndicationNodeFactory { /** Use [GlimmerTheme.LocalGlimmerTheme] to access this publicly. */ @Suppress("CompositionLocalNaming") -private val _localGlimmerTheme: ProvidableCompositionLocal = - staticCompositionLocalOf { +privalGlimmerTheme: ProvidableCompositionLocalGlimmerTheme = + stLocalOf { GlimmerTheme() } +GlimmerTheme.kt ```
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md index 59f9e71..0f199a2 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md @@ -205,12 +205,6 @@ private fun ListItemImpl( val iconSize = GlimmerTheme.iconSizes.large val typography = GlimmerTheme.typography val innerPadding = GlimmerTheme.componentSpacingValues.small - val depthEffect = - SurfaceDepthEffect( - depthEffect = null, - focusedDepthEffect = GlimmerTheme.depthEffectLevels.level4, - ) - val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } val surfaceModifier = @@ -218,7 +212,8 @@ private fun ListItemImpl( shape = shape, color = color, contentColor = contentColor, - depthEffect = depthEffect, + // TODO(b/532516157): Reenable depthEffect after b/446294492 is fixed. + depthEffect = null, border = border, interactionSource = internalInteractionSource, ) From 57ff3c7d02a53781954f9ce2df92f14b7fbb2ded Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 10 Jul 2026 16:33:36 +0000 Subject: [PATCH 31/61] Updates skills (2026-07-10 16:33) --- play/play-policy-insights/SKILL.md | 169 +++ .../resources/common_mandates.md | 100 ++ .../resources/compliance_report_template.md | 58 + play/play-policy-insights/resources/critic.md | 86 ++ .../resources/goal_data_safety.md | 212 +++ .../resources/goal_permissions_and_apis.md | 459 +++++++ .../resources/goal_user_account.md | 106 ++ .../resources/policies.json | 321 +++++ .../resources/scanner_config.json | 799 +++++++++++ .../scripts/generate_report.py | 860 ++++++++++++ .../scripts/orchestrator.py | 1207 +++++++++++++++++ .../scripts/play_store_scraper.py | 410 ++++++ play/play-policy-insights/scripts/scanner.py | 259 ++++ .../scripts/template_engine.py | 243 ++++ 14 files changed, 5289 insertions(+) create mode 100644 play/play-policy-insights/SKILL.md create mode 100644 play/play-policy-insights/resources/common_mandates.md create mode 100644 play/play-policy-insights/resources/compliance_report_template.md create mode 100644 play/play-policy-insights/resources/critic.md create mode 100644 play/play-policy-insights/resources/goal_data_safety.md create mode 100644 play/play-policy-insights/resources/goal_permissions_and_apis.md create mode 100644 play/play-policy-insights/resources/goal_user_account.md create mode 100644 play/play-policy-insights/resources/policies.json create mode 100644 play/play-policy-insights/resources/scanner_config.json create mode 100755 play/play-policy-insights/scripts/generate_report.py create mode 100755 play/play-policy-insights/scripts/orchestrator.py create mode 100755 play/play-policy-insights/scripts/play_store_scraper.py create mode 100755 play/play-policy-insights/scripts/scanner.py create mode 100644 play/play-policy-insights/scripts/template_engine.py diff --git a/play/play-policy-insights/SKILL.md b/play/play-policy-insights/SKILL.md new file mode 100644 index 0000000..a562751 --- /dev/null +++ b/play/play-policy-insights/SKILL.md @@ -0,0 +1,169 @@ +--- +name: play-policy-insights +description: Automated auditor designed to verify Android applications against Google Play Policy domains. It cross-references static code analysis with Play Store declarations to generate deterministic compliance reports, identifying undeclared data collection, architectural risks, and missing disclosures across Permissions and APIs Hygiene, User Account and Identity, and Data Safety and Privacy domains. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-10' + keywords: + - account deletion + - accessibility api + - all files access + - audio recording + - audit + - compliance + - contacts access + - data disclosure + - data safety + - data safety label + - data transmission + - demo credentials + - exact alarm + - foreground services + - location access + - login credentials + - manifest hygiene + - package visibility + - permissions hygiene + - photo and video access + - photopicker + - play policy + - pre-submission audit + - privacy policy + - prominent disclosure + - restricted permissions + - scoped storage + - sms and call log + - static analysis + - target sdk + - user consent +--- + +# Play Policy Insights: data safety, login credentials, and restricted permissions + +You must audit Android apps for three specific policy domains. You must check +data safety, demo login credentials, and restricted permissions. + +## Path Resolution + +* **repo_root**: Absolute path to the directory containing this `SKILL.md`. +* **app_dir**:: Absolute path to the directory containing app's code. +* **temp_dir**: Absolute path to the scratch directory at the workspace root. + It is located at `.scratch/play_policy_insights_`. **Containment + Mandate**: You must confine all file system writes, intermediate artifacts, + and logs strictly to this directory. This ensures the skill remains portable + and safe across diverse execution environments, including local harnesses + and CI/CD pipelines, by avoiding reliance on system-level temporary paths or + user home directories. + +## Critical mandates + +- **Execution Mode Awareness** Before starting Phase 2, evaluate if your + execution environment provides a tool to spawn or delegate tasks to + general-purpose sub-agents (e.g., tools often named `invoke_agent`, + `delegate_task`, or `spawn_worker`, using generic agent profiles like + 'generalist' or 'coding_agent'). + +- If **YES**, you MUST use **Mode A (Delegation)**. + +- If **NO**, use **Mode B (Sequential Self-Execution)**. You must read the + prompt files intended for the subagents, follow their instructions, and + write the expected output files to disk. + +- **Sub-agents orchestration:** + + - If you use "Mode A (Delegation)", wait for "SUCCESS" confirmation from + sub-agents to know when they are done. + + - **Idempotency & Timeout Safeguard**: If a sub-agent fails or times out, + you MUST verify the presence and integrity of its target output file + (e.g., `/worker_.json`) before retrying. If the + file exists and contains valid JSON, treat the execution as **SUCCESS** + and proceed. Otherwise, retry up to three times. + +- **Fail-fast mandate:** The automated audit in Phase 1 is the source of + truth. If `orchestrator.py` fails, you must stop immediately with an + explanation of failure. Do not use manual auditing as a fallback. + +## The two-phase protocol + +### Phase 1: Fact gathering and triage + +1. **Initialize and triage**: + - Run `python3 /scripts/orchestrator.py init `. + - This will create the scratch environment, perform static analysis, map + the codebase, identify audit goals, and produce prompts for subagents + for each audit goal and prompts for designated critic and aggregator + subagents. + - You must wait (up to 5 minutes) for the script to finish. +2. **Capture environment**: Note values of the `temp_dir`, and + `activated_goals` from the JSON output. You will need them in Phase 2. +3. **Evaluate goals**: If `activated_goals` is empty, skip to step 3 of Phase 2 + (Aggregation). Otherwise, proceed to step 1 of Phase 2 (Detailed analysis). + +### Phase 2: Goal-oriented audit + +Determine your execution capabilities and proceed with either Mode A OR Mode B. + +#### Mode A: Orchestrator WITH Delegation Capabilities (Parallel) + +1. **Detailed analysis**: For each goal in `activated_goals` (e.g., + `permissions_and_apis`, `data_safety_part_1`, `data_safety_part_2`), + delegate to a sub-agent. **Concurrency Limit:** You must not spawn more than + 3 sub-agents simultaneously. Spawn the first batch of up to 3, wait for + their completions, and then spawn the next batch. Repeat until all goals are + complete. Pass the prompt: `"Read your instructions from + /prompt_worker_.md and execute. MANDATORY: You must + use your file-writing capabilities to save your final JSON findings directly + to the file system at /worker_.json. You are strictly + forbidden from outputting the JSON in your chat response. To minimize + context usage, your final response must be exactly 'SUCCESS' and nothing + else."` **Validate**: Confirm every + `/worker_.json` exists and contains valid JSON. If a + sub-agent fails or times out, but the valid JSON output file is already + present on disk, do NOT retry; proceed normally. Only retry the + corresponding worker (up to three times) if the file is missing or invalid. +2. **Aggregate Findings**: Execute the python aggregation command: + `python3 /scripts/orchestrator.py aggregate `. This + produces `aggregated_findings.json` and returns a JSON object containing + `critic_chunks` representing the number of chunks to verify (e.g., + `{"temp_dir": "...", "critic_chunks": 2}`). +3. **Parallel Critic review**: For each chunk index `i` from 1 to + `critic_chunks`, delegate to a sub-agent. **Concurrency Limit:** You must not + spawn more than 3 critic sub-agents simultaneously. Batch them in groups of 3 + as above. Pass the prompt: + `"Read your instructions from /prompt_critic_.md and execute. MANDATORY: You must use your file-writing capabilities to save your final JSON findings directly to the file system at /critic_output_.json. You are strictly forbidden from outputting the JSON in your chat response. To minimize context usage, your final response must be exactly 'SUCCESS' and nothing else."` + **Validate**: Confirm each `/critic_output_.json` exists and + contains valid JSON before proceeding. If it failed or timed out, but the + valid JSON file is present, proceed normally. Otherwise, retry that specific + critic chunk. +4. **Proceed to Finalization** (Step 4 below) + +#### Mode B: Orchestrator WITHOUT Delegation Capabilities (Sequential) + +1. **Detailed Analysis**: For each goal in `activated_goals`, sequentially: + - Read the contents of `/prompt_worker_.md`. + - Execute the instructions contained within that file yourself. + - **CRITICAL**: You MUST format your findings exactly as requested in the + prompt and save them to `/worker_.json`. **Do not** + summarize findings in your thoughts or chat; move to the next task. + - **Validate**: Confirm `/worker_.json` exists before + moving to the next goal. +2. **Aggregate Findings**: Execute the python aggregation command: + `python3 /scripts/orchestrator.py --aggregate `. + This produces `aggregated_findings.json` and returns a JSON object containing + `critic_chunks` representing the number of chunks to verify. +3. **Sequential Critic review**: For each chunk index `i` from 1 to + `critic_chunks`, sequentially: + - Read the contents of `/prompt_critic_.md`. + - Execute the steps yourself and save your findings to + `/critic_output_.json`. + - **Validate**: Confirm `/critic_output_.json` exists before + moving to the next chunk. +4. **Proceed to Finalization** (Step 4 below) + +#### Finalization (Both Modes) + +4. **Present findings**: Run `python3 /scripts/generate_report.py `. + It will produce `/compliance_report.md`. Present this output file to user. +5. **STOP**: The audit is complete. Await further instructions. diff --git a/play/play-policy-insights/resources/common_mandates.md b/play/play-policy-insights/resources/common_mandates.md new file mode 100644 index 0000000..87857e1 --- /dev/null +++ b/play/play-policy-insights/resources/common_mandates.md @@ -0,0 +1,100 @@ +# Execution Mandates + +### Technical Rules + +1. **Absolute Paths Only**: Always resolve and use absolute paths. +2. **Containment**: Write all artifacts strictly within `{{TEMP_DIR}}`. +3. **Fail-fast**: If any required input file is missing, stop immediately and + report the failure. + +### Surgical Input Protocol & Efficient Search (MANDATORY) + +- **Direct Evidence First**: Prioritize files listed in the **Context & + Evidence** sections. Use the provided file/line evidence (e.g., from Data + Sources or Sinks) to jump directly to the relevant code. Do not perform + broad workspace searches if these surgical starting points are available. +- **Path Filtering Over File Crawling**: Locate target files by name, path, or + extension *first* using directory/file listing tools before performing any + text/content-based searches. Restrict searches and file reads strictly to + the target `{{APP_DIR}}`. +- **Strict Exclusions (The Noise Wall)**: Configure search, glob, and find + tools to ignore build, cache, dependencies, and testing folders. You MUST + exclude matches from: `**/build/**`, `**/.gradle/**`, `**/.scratch/**`, + `**/androidTest/**`, `**/test/**`, `**/node_modules/**`. +- **Targeted Extensions**: Restrict content searches and file reads strictly + to source and configuration files: `.java`, `.kt`, `.xml`, `.gradle`, `.kts` + (and `.js`, `.ts`, `.jsx`, `.tsx`, `.dart` if a hybrid/cross-platform + environment is analyzed). Never search or read inside compiled `.class` + files, binary resources, or output assets. +- **Surgical Queries & Limiters**: Use highly specific search patterns (e.g., + search for `getLastKnownLocation` or `deleteAccount` instead of general + words like `location` or `delete`). If search tools support limits or + pagination, cap results at a maximum of 50 matches. Do not load unlimited + search results into your context window. +- **Parallel Reading Required (Turn Efficiency)**: You are operating under a + strict maximum turn limit. To prevent timeouts, you MUST request to read + multiple target files concurrently in a single response. Do not read the + evidence files sequentially one-by-one. Issue all of your file-reading tool + calls simultaneously whenever possible. + +### Evidentiary Standard & Guardrails (CRITICAL) + +To prevent over-auditing, false positives, and speculative "prosecution" of +compliant code during extrapolation: + +1. **Presumption of Compliance**: Treat code as compliant unless there is + *definitive, visible evidence* in the provided files of a policy violation. + If code is ambiguous, or if network/database logic is hidden behind + abstractions (e.g., calling an interface or repository method like + `clearSession()`), you must assume standard compliant behavior. Do NOT guess + or speculate about what happens behind interfaces. +2. **Benefit of the Doubt**: When compliance cannot be strictly verified due to + code abstractions or missing source file contexts, you must downgrade your + finding: + - Never flag a `🔴 Critical` or `🟡 Important` finding based on suspicion or + lack of context. + - Instead, output a `🔵 Suggestion` (informational) to advise the developer + on what to double-check in their backend or configuration. +3. **Exclusion of Local State**: Local-only processing (e.g., caching theme + settings, user-selected visual configurations, or on-device-only database + operations) is explicitly exempt from Data Safety collection or Account + Deletion mandates. +4. **Concrete Attributions**: Every `🔴 Critical` or `🟡 Important` finding must + cite the exact file, line number, or configuration block containing the + direct violation. If you cannot cite the exact line of code containing the + violation, you cannot flag it as a violation. +5. **Empty-List Discipline**: If no policy violations, discrepancies, or review + items are identified during your audit, you MUST represent this as an empty + array `[]` for that field (e.g., `"findings": []`, `"verified_findings": + []`, or `"manual_verification_required": []`). **DO NOT** populate arrays + with "dummy" objects, placeholder strings, or `"N/A"` / `"None"` values. +6. **Heuristics & Extrapolation Boundaries**: Whenever applying specific + heuristics defined in your goal (e.g., searching for implicit logger leaks + or + SDK siphoning), you must strictly bound them to the provided evidence and + their immediate callers. You are strictly forbidden from initiating broad, + unbounded searches for custom paths or variables across the wider codebase. + Base your extrapolation only within the specific files already provided to + you in the prompt. + +### Finalization & Output Mandates (CRITICAL) + +- **Iterative Saving**: If your investigation requires multiple steps, save + partial or intermediate JSON states to disk as you progress. Do not hold all + data in memory until the very end to prevent data loss upon interruption. +- **Strict File Output (NO TRIPLE BACKTICKS)**: You MUST save your final JSON + output to disk at the exact path specified in the goal schema using your + file-writing capabilities. + - **CRITICAL: The content written to the file MUST be pure, raw JSON. DO + NOT wrap the contents inside the JSON file with Markdown code blocks + (such as triple backticks `json ...`). Writing markdown blocks into the + file makes the JSON unparseable by the compiler.** +- **NO Chat Summaries**: **MANDATORY: DO NOT summarize your findings, explain + your reasoning, or output JSON in your final chat response.** Your chat + output wastes context and is ignored by the orchestrator. +- **Verification Before Termination**: You MUST only terminate and return the + "SUCCESS" string *after* you have explicitly verified that your JSON file + successfully wrote to disk and contains valid JSON (e.g., by reading the + file back or checking the directory contents). +- **Final response**: Your final response MUST be exactly the word: "SUCCESS" + and nothing else. diff --git a/play/play-policy-insights/resources/compliance_report_template.md b/play/play-policy-insights/resources/compliance_report_template.md new file mode 100644 index 0000000..e9854c9 --- /dev/null +++ b/play/play-policy-insights/resources/compliance_report_template.md @@ -0,0 +1,58 @@ +# Play Policy Compliance Report: {{app_name_id}} + +**Status:** {{overall_compliance}} **Scan Date:** {{current_date}} + +> **Disclaimer:** This report is generated by an AI automated scanner and is for + **informational and advisory purposes only**. It does NOT constitute legal + advice, a guarantee of Play Store approval, or a definitive compliance check. + The Google Play Review team has final authority. Undiscovered issues may still + exist. + +--- + +## Policy Risks & Recommendations + +*Review these identified risks and follow the suggested recommendation steps.* + +{{findings_detail}} + +--- + +## Data Safety Discrepancies + +Review these discrepancies between detected behavior and Play Store +declarations. Discrepancies may stem from data collection/sharing practices or +new code features not yet reflected in your Data Safety label. + +{{data_safety_table}} + +{{suggested_declaration_section}} + +{{local_access_section}} + +--- + +## Remediation Checklist + +Follow these personalized steps to resolve the identified risks. + +{{personalized_checklist}} + +--- + +### Play Store Compliance Status Legend + +| Overall Status | Meaning & Rejection Risk | Required Action | +| :--- | :--- | :--- | +| **🟢 Compliant** | The application code and metadata appear fully compliant with analyzed Play Store policies. No active policy violations or high-risk discrepancies were identified. | **Safe to Submit**: Safe for Play Store submission. | +| **🟡 Needs Review** | No critical violations were found in the code, but the app implements features requiring manual Play Console configurations (e.g., submitting **Reviewer Demo Credentials** for login screens) or requests restricted, high-risk permissions (e.g., Exact Alarms, All Files Access, or Background Location) requiring Play Console declaration forms. | **Must Review**: Review the findings. You must either **migrate to modern scoped alternatives (highly recommended)** to bypass declaration scrutiny, OR **manually verify that your Play Console settings (like Reviewer Credentials and Declaration Forms) are completely and carefully configured** before submitting. | +| **🔴 Non-Compliant** | Severe, direct policy violations (e.g., silent background data tracking, default-only permission abuse) or active Data Safety declaration mismatches were identified. | **Must Fix**: Immediate rejection or account-level policy suspension risk. You must remediate these issues before submitting to the Play Store. | + +
+ +| Finding Severity | Description | Action Required | +| :--- | :--- | :--- | +| **🔴 Critical** | Direct, verified policy violation (e.g., unauthorized data collection/sharing). | **Must Fix**: Immediate rejection risk. | +| **🟡 Important** | Deprecated permission use or API discrepancy where scoped pathways are mandated. | **Should Fix**: Highly recommended to migrate to prevent manual reviewer flags. | +| **🔵 Suggestion** | Standard advisory reminder or administrative checklist item (fully compliant). | **Good to Know**: Clean-path confirmation or helper checklist. | + diff --git a/play/play-policy-insights/resources/critic.md b/play/play-policy-insights/resources/critic.md new file mode 100644 index 0000000..f39d7b5 --- /dev/null +++ b/play/play-policy-insights/resources/critic.md @@ -0,0 +1,86 @@ +# Review of specialized policy findings (Chunked Critic) + +Review a specific chunk of identified policy findings to identify potential +false positives, exaggerated claims, or inaccuracies due to lack of evidence. + +### Provided context files + +The prompt provides absolute paths to files and directories: +- `{{TEMP_DIR}}`: The temporary scratch directory for this audit. +- `input_file`: `{{TEMP_DIR}}/input_critic_{{CHUNK_INDEX}}.json` contains the + chunk of findings to verify. + +### Instructions + +1. **Read input chunk**: Read the JSON object in + `{{TEMP_DIR}}/input_critic_{{CHUNK_INDEX}}.json`. The keys (e.g. `"1"`, + `"2"`, `"3"`) correspond to the finding IDs. +2. **Verify each finding**: + - For each finding, analyze its evidence against the source files in + `{{APP_DIR}}`. + - Determine the verdict: + - `"VERIFIED"`: True Positive. The codebase confirms the policy + violation. + - `"MANUAL_REVIEW"`: Ambiguous code or abstract logic where automatic + verification is impossible. + - `"PRUNED"`: False Positive. The codebase is compliant, or the + finding is not supported by actual evidence. + - Provide your verification details in the output JSON. +3. **Optional Editorial Overrides**: If (and only if) you need to edit, refine, + or moderate the worker finding's text, you may include one or more of these + optional keys to your decision object. **If you agree with the worker's text, + you MUST omit these keys entirely from your JSON.** + - `"issue_summary"`: Write a more accurate, tailored summary. + - `"severity"`: Set to `"CRITICAL"`, `"IMPORTANT"`, or `"SUGGESTION"` to + override. + - `"recommendation"`: Write a tailored, codebase-specific remediation + step. +4. **Save Results**: Save your final "Thin JSON" mapping to + `{{TEMP_DIR}}/critic_output_{{CHUNK_INDEX}}.json`. + +### Audit principles for false positive detection + +- **Environment detection**: **Do not** flag emulator or root detection as + violations unless evidence shows malicious intent or review evasion. +- **Speculative collection**: Prune claims based solely on permission + presence. Require evidence of actual data access and transfer (whether + off-device network egress or on-device sharing to a third-party app). +- **Standard patterns**: **Do not** flag standard Android architectural + patterns unless used maliciously. +- **Surgical Evidence Standard**: Prune or downgrade any finding where the + worker has speculated on a transmission pathway that cannot be directly + verified in the immediate source files, or where the finding fails to cite a + concrete file and line number containing the direct policy violation. +- **Data Safety Flag Verification**: If a finding includes Data Safety flags + (e.g., `user_initiated`, `is_third_party`), strictly verify them. If the + worker claims `user_initiated: true`, ensure there is undeniable evidence of + explicit user interaction triggering the transfer. If the worker claims + `is_third_party: true`, verify the sink is definitively outside the + developer's control (e.g., Android Share Sheet, Social Media SDK). If evidence + is lacking, downgrade the finding or use `"issue_summary"` to correct the + claim. +- **Gatekeeper Validation**: If a worker claims a finding is compliant because + a disclosure exists (`prominent_disclosure_status: "DISCLOSED"`), you MUST + verify that the UI acts as a strict gatekeeper. If data collection begins + before the user taps "Accept", or if they can dismiss it and continue, + override the `"severity"` to `"CRITICAL"` and explicitly state the gatekeeper + is invalid. + +### Output JSON format + +**Important**: The output must be a pure JSON object mapping the sequential +finding IDs from your input file to their decisions. If there are no findings in +the chunk, return `{}`. + +```json +{ + "1": { + "action": "VERIFIED | MANUAL_REVIEW | PRUNED", + "confidence": "High | Medium | Low", + "critic_justification": "A concise, technical explanation of your decision based on codebase evidence.", + "issue_summary": "OPTIONAL: Overridden issue summary text", + "severity": "OPTIONAL: CRITICAL | IMPORTANT | SUGGESTION", + "recommendation": "OPTIONAL: Overridden recommendation text" + } +} +``` diff --git a/play/play-policy-insights/resources/goal_data_safety.md b/play/play-policy-insights/resources/goal_data_safety.md new file mode 100644 index 0000000..59a9337 --- /dev/null +++ b/play/play-policy-insights/resources/goal_data_safety.md @@ -0,0 +1,212 @@ +{{#IF_ANY data_sources, disclosure}} +{{ACTIVATE_GOAL}} +{{/IF_ANY}} +## Data Safety and Privacy Audit + +### Policies to Verify + +- **The Policy Spirit**: Users deserve complete transparency regarding what + personal or sensitive information leaves their devices. Under Play Store + rules, any off-device transmission of user data must be explicitly declared in + the Play Store Data Safety form, and sensitive or non-obvious data collection + requires prior prominent in-app disclosure and affirmative user consent. + +- **Common Evaluation Matrix**: + | Case | Technical Observation | `user_initiated` | `is_third_party` | Disclosure Status | Severity | Issue Summary | + | :--- | :--- | :--- | :--- | :--- | :--- | :--- | + | **1** | **Not Transferred** (Local only) | N/A | N/A | `EXEMPT` | `SUGGESTION` | "Data Safety Compliant (Local-Only)" | + | **2A** | **Transferred** + **Background** + **No Disclosure** | `false` | (Any) | `MISSING` | `CRITICAL` | "**Silent Background Transfer**": Policy violation. Data is sent without prior disclosure. | + | **2B** | **Transferred** + **Background** + **Disclosure Found** | `false` | (Any) | `DISCLOSED` | `IMPORTANT` | "**Background Transfer (Disclosure Claimed)**": Evidence found; requires Critic verification. | + | **3** | **Transferred** + **User-Initiated** + **First Party** | `true` | `false` | `EXEMPT` | `IMPORTANT` | "**User-Initiated Collection**": Disclosure exempt, but **must be declared** in Play Store form. | + | **4** | **Transferred** + **User-Initiated** + **Third Party** | `true` | `true` | `EXEMPT` | `SUGGESTION` | "**Manual Sharing**": **Policy Exempt**. User triggers transfer to 3P; no disclosure or declaration needed. | + +- **Exemptions Reference**: + - **Anonymous Data**: Fully anonymized data not linked to a user is exempt. + - **End-to-End Encryption (E2EE)**: Data E2EE where the developer cannot read + it is exempt. + - **Open Web WebView**: Data entered into a WebView navigating the open web is + exempt. + - *If any of these apply, downgrade severity to `SUGGESTION` and mark + `EXEMPT`.* + +--- + +### The Audit Protocol + +#### Step 0: Facts orientation + +- **Identify Third-Party SDKs**: If you observe network, crash-reporting, or + tracking telemetry, check the project's build files (e.g., build.gradle, + build.gradle.kts, libs.versions.toml) on-demand using your file-reading tools + to verify which SDKs (Analytics, Ads, etc.) are integrated. +- **Understand Off-Device Sinks**: Use your native knowledge of standard Android + framework components to identify transmission destinations. Focus on active + egress vectors such as standard HTTP/REST libraries (e.g., OkHttp, Retrofit, + Ktor, HttpURLConnection), WebViews passing data off-device, background syncing + tasks, or analytic/telemetry frameworks (e.g., Firebase, Mixpanel, Adjust, + AppsFlyer). +- **Identify Disclosures**: Review `disclosure` below. Use these as starting + points for Step 2. + +#### Step 1: Verify behavioral transmission (Forward Trace) + +For each source in `data_sources` listed below, trace the technical signal from +the exact location provided to its transmission endpoint. +- **Mandatory Starting Point**: Navigate to the file and line number specified + in the `data_sources` list. +- **Loop Discipline**: You must analyze the transmission path for *every* piece + of evidence listed to verify if it reaches an off-device sink. +- **Evidence**: Provide the file/line where the data is passed to the sink and a + brief justification of the sink's known transmission behavior (e.g., network + upload, telemetry payload). +- **Semantic Grounding**: Use the provided `*Description*` to verify that the + code evidence actually matches the semantic scope of the data type. If the + variable or logic refers to unrelated data (e.g., a `fileName` that does not + identify a user `NAME`), treat it as a false positive and follow the + **Evidentiary Standard** to downgrade or prune the finding. +- **Sink Categorization**: Determine the destination: + 1. **First Party / Service Provider (`is_third_party: false`)**: + Developer-controlled servers, Firebase, custom APIs, etc. + 2. **Third Party (`is_third_party: true`)**: Social SDKs, ad networks, or + user-selected apps via Android Share Sheet (`Intent.ACTION_SEND`). +- **Ambiguity Mandate**: If the destination is ambiguous (e.g., generic URL or + obfuscated SDK), you MUST default to `is_third_party: false` (erring on the + side of Collection). +- **On-Device Transfer**: Treat silent data sharing to another app via + Intents/ContentProviders as a transfer (`is_transferred: true`), even if it + doesn't use the network. + +#### Step 2: Semantic Search for Protection (UI Disclosure) + +**Conditional Execution**: Execute this step **ONLY IF** `is_transferred` is +`true` AND `user_initiated` is `false`. +- **Rationale**: User-initiated actions (Case 3 and 4) are exempt from Prominent + Disclosure. Do not search for disclosures if `user_initiated` is true. +- **Semantic Search**: Review the `disclosure` list below. +- **Gatekeeper Check**: Check if the Activity/Fragment displaying that consent + acts as a gatekeeper (i.e., gates the initialization of the data-collection + logic). Do not attempt to trace backwards from a low-level repository up to + the UI. +- **Content Check**: The UI must state *what* is collected and *how* it is used. +- **Evidence**: Provide the file/layout name and the specific text found. + +#### Step 3: Synthesis + +- **`findings` (CRITICAL)**: You must construct and output exactly **one** + object inside the `"findings"` array for **every** data type found in the + `data_sources` list below, regardless of whether it is compliant or a + violation. +- **Evaluating Keys**: For each data type object in `"findings"`, evaluate and + populate: + 1. **psl_constant**: Set this exactly to the uppercase **PSL Constant** of + the data type (listed as the **Data Type** heading in the **Data Sources to + Trace** section below, e.g., `"PRECISE_LOCATION"`, `"CONTACTS"`). This is + critical for downstream matching. + 2. **policy_id**: Set this exactly to either `"prominent_disclosure_policy"` + or `"data_safety_section"` depending on the check being performed. + 3. **is_transferred**: `true` (JSON Boolean) if the data is transferred + off-device or on-device to a third party. `false` (JSON Boolean) if local + only. + 4. **user_initiated**: `true` (JSON Boolean) if the user explicitly triggers + the transfer. + 5. **is_third_party**: `true` (JSON Boolean) if the destination is a Third + Party (e.g., Share Sheet, Social SDK). `false` (JSON Boolean) if it's a + First Party/Service Provider (e.g., your own backend). + 6. **prominent_disclosure_status**: Set strictly to one of these three + uppercase enums: `"DISCLOSED"` (visible disclosure and user consent found), + `"MISSING"` (no disclosure found, or is not an affirmative gatekeeper), or + `"EXEMPT"` (exempt from prominent disclosure under policies). + 7. **purpose**: Categorize why the data is being collected (e.g., "App + functionality", "Analytics", "Local functionality only"). + 8. **linked_to_user**: `true` (JSON Boolean) if the data is tied to an + identity, email, or device ID, otherwise `false` (JSON Boolean). + +- **Map findings strictly to the 5 Cases:** Use the Case matrix in "Policies to + Verify" to set the appropriate `severity` and `issue_summary` based on the + combination of `is_transferred`, `user_initiated`, `is_third_party`, and + `prominent_disclosure_status`. + +--- + +### Domain-Specific Heuristics + +Apply the following heuristics while strictly adhering to the Heuristics & +Extrapolation Boundaries defined in your Execution Mandates: +1. **Implicit Data Leaks (The Logger Loop)**: Inspect if any custom logging + frameworks (e.g., Crashlytics, custom error loggers, or telemetry SDKs) + receive sensitive variables as part of diagnostic payloads. If a user + identifier (e.g., email, account ID) or precise location is passed to a + logger that uploads payloads off-device, this counts as **transferred** + (`is_transferred`: true). +2. **Third-Party SDK Siphoning**: Look at the project's build files on-demand + (e.g., build.gradle or libs.versions.toml). If SDKs like Google Ads, or + Firebase are initialized and have access to the context, evaluate if + they are siphoning advertiser IDs or device identifiers automatically. If + those libraries are loaded and the manifest requests broad network + permissions, treat those device identifiers as **transferred** + (`is_transferred`: true) for analytics/marketing purposes. +3. **Indirect Consent**: If you find an `AlertDialog` or disclosure, verify if + it is an actual gatekeeper. If the app begins tracking user data *prior* to + the user tapping "Accept", or if the user can close the dialog and continue + using the app while data tracking remains active, flag this as a `CRITICAL` + violation under `prominent_disclosure_policy`. + +--- + +### Codebase Context & Evidence + +{{#IF semantic_files.LEGAL}} +**Semantic Triage Starting Points (Files of Interest)**: +The following files likely contain privacy or consent logic: +- **LEGAL** related: + {{#EACH semantic_files.LEGAL}} + - `{{ITEM}}` + {{/EACH}} +{{/IF}} + +{{#IF data_sources}} +**Data Sources to Trace**: +{{#EACH data_sources}} +- **Data Type**: {{KEY}} + *Description: {{VALUE.description}}* + {{#EACH VALUE.findings}} + - `{{ITEM}}` + {{/EACH}} +{{/EACH}} +{{/IF}} + +{{#IF disclosure}} +**UI Disclosure Starting Points**: +{{#EACH disclosure}} +- `{{ITEM}}` +{{/EACH}} +{{/IF}} + +## Output schema + +Save final JSON output to `{{TEMP_DIR}}/worker_{{GOAL_NAME}}.json`. + +```json +{ + "domain": "Data Safety and Privacy", + "findings": [ + { + "psl_constant": "STRING_VALUE (The exact PSL Constant, e.g., USER_ACCOUNT)", + "policy_id": "prominent_disclosure_policy | data_safety_section", + "issue_summary": "STRING_VALUE", + "severity": "CRITICAL | IMPORTANT | SUGGESTION", + "files_involved": ["STRING_VALUE"], + "evidence": "STRING_VALUE", + "recommendation": "STRING_VALUE", + + # UNIFIED DATA SAFETY METADATA LOOKUP KEYS: + "is_transferred": true | false, + "user_initiated": true | false, + "is_third_party": true | false, + "prominent_disclosure_status": "DISCLOSED | MISSING | EXEMPT", + "purpose": "STRING_VALUE", + "linked_to_user": true | false + } + ] +} +``` diff --git a/play/play-policy-insights/resources/goal_permissions_and_apis.md b/play/play-policy-insights/resources/goal_permissions_and_apis.md new file mode 100644 index 0000000..f933005 --- /dev/null +++ b/play/play-policy-insights/resources/goal_permissions_and_apis.md @@ -0,0 +1,459 @@ +{{#IF_ALL requested_permissions.exact_alarm, exact_alarm}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.foreground_service, foreground_service}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.accessibility, accessibility}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.APPS_ON_DEVICE, data_sources.APPS_ON_DEVICE}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.SMS_CALL_LOG, data_sources.SMS_CALL_LOG}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.MEDIA, data_sources.MEDIA}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.AUDIO, data_sources.AUDIO}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.ALL_FILES, data_sources.FILES_AND_DOCS}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.LOCATION, data_sources.PRECISE_LOCATION}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.LOCATION, data_sources.APPROX_LOCATION}}{{ACTIVATE_GOAL}}{{/IF_ALL}} +{{#IF_ALL requested_permissions.CONTACTS, data_sources.CONTACTS}}{{ACTIVATE_GOAL}}{{/IF_ALL}} + +## Permissions and APIs Audit + +### Hint: Deducing Core Functionality + +Since you must determine if certain permissions are justified by the app's "core +purpose", use these fast heuristics: +1. **The "Broken" Test**: Is the feature essential to the app's primary purpose? + If the app would still be functional and useful without the feature, it is + NOT core functionality. +2. **Manifest Intent**: Review `AndroidManifest.xml`. The name of the `LAUNCHER` + Activity and specialized `` declarations (like default SMS + handlers) strongly indicate the app's main purpose. +3. **Naming**: The package name (`{{PACKAGE_NAME}}`) and app label + (`{{APP_NAME}}`) often describe the app's purpose explicitly. +4. **Execution Context**: Usage in classes like `BackupManager` suggest core + functionality, whereas usage in `AdHelper`, `CrashReporter`, or + `AnalyticsManager` indicates secondary features. +5. **Mandatory Rule**: Secondary features like **advertising, analytics, or + social sharing never justify** restricted permissions like Background + Location, All Files Access, or Broad Media Access. + +--- + +### Policies to Verify + +{{#IF_ALL requested_permissions.MEDIA, data_sources.MEDIA}} +#### Photo and Video Access Policy (Policy ID: photo_video_access_policy) + +- **Goal**: Evaluate if the app's core functionality justifies broad access to + photos or if it should migrate to the Android Photo Picker. +- **The Policy Spirit**: User privacy is paramount. Apps should only request + broad media storage permissions if they are dedicated media managers (like + Gallery or Backup apps). For standard tasks like profile picture uploads, + custom sharing, or attaching media, developers must use scoped APIs to prevent + security risks. +- **Evidence**: + {{#EACH data_sources.MEDIA}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.MEDIA}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Target SDK | Broad Media Permission Requested? | Condition / Context Checked | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **33 or higher** | Yes | App requests broad media access (e.g. `READ_MEDIA_IMAGES`), but features only require user-selected media. | `IMPORTANT` | Migrate to the **Android Photo Picker** (`MediaStore.ACTION_PICK_IMAGES`) which does not require any permission prompt. | + | **Any** | Yes | App requests broad storage/media permissions but is not a dedicated media manager (e.g., a social or utility app). | `IMPORTANT` | Migrate to the **Android Photo Picker** for single-item or multi-item media selection. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Look beyond standard native file pickers. Extrapolate using this heuristic: + 1. **User-Selected Media Heuristic**: Analyze where the code handles selected + files. If images or videos are loaded strictly via a user-facing button + click (e.g., "Upload Avatar", "Share Photo", "Attach File") and processed + one-at-a-time, broad filesystem media permissions are structurally + unnecessary. Flag an `IMPORTANT` violation and recommend Photo Picker + migration. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.ALL_FILES, data_sources.FILES_AND_DOCS}} +#### All Files Access Policy (Policy ID: all_files_access_policy) + +- **Goal**: Evaluate if the app's core purpose justifies the high-risk + `MANAGE_EXTERNAL_STORAGE` permission. +- **The Policy Spirit**: Full filesystem access is a restricted privilege. The + Play Store strictly limits `MANAGE_EXTERNAL_STORAGE` to apps where full disk + reads/writes are critical to the core purpose (e.g., file managers, antivirus + scanner, backup tools). Non-compliant apps must utilize Scoped Storage or + Storage Access Framework (SAF). +- **Evidence**: + {{#EACH data_sources.FILES_AND_DOCS}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.ALL_FILES}} + - `{{ITEM}}` + {{/EACH}} +- **Common Evaluation Matrix**: + | Core App Purpose | Is Permission Justified? | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **File Manager, Antivirus, Backup/Restore, or Document Manager** | Yes (Compliant) | None | No action needed; core purpose justifies the restricted permission. | + | **Standard Utility, Social Media, Game, or Productivity App** | No (Violation) | `CRITICAL` | Remove `MANAGE_EXTERNAL_STORAGE` from the Manifest. For document picking or local file saving, migrate to the **Storage Access Framework (SAF)**. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Surgically audit how the filesystem is queried: + 1. **Scoped Storage Sufficiency**: Check the files using the storage APIs. If + the app is using filesystem pathways solely to log diagnostics, store + custom caches, or save simple download artifacts, broad access is not + justified. Recommend using app-specific directories + (`Context.getExternalFilesDir()`) which require zero permissions. +{{/IF_ALL}} + +{{#IF requested_permissions.LOCATION}} +{{#IF_ANY data_sources.PRECISE_LOCATION, data_sources.APPROX_LOCATION}} +#### Location Access Policy (Policy ID: location_access_policy) + +- **Goal**: Verify that location access is essential, uses minimum scope, and is + properly disclosed. +- **The Policy Spirit**: User tracking is extremely sensitive. Apps must collect + the minimum scope of location required (approximate vs precise), provide clear + prominent disclosures, and must never utilize background tracking unless it is + essential for safety, navigation, or physical fitness features. +- **Evidence**: + {{#IF data_sources.PRECISE_LOCATION}} + - **Precise Location usage**: + {{#EACH data_sources.PRECISE_LOCATION}} + - `{{ITEM}}` + {{/EACH}} + {{/IF}} + {{#IF data_sources.APPROX_LOCATION}} + - **Approximate Location usage**: + {{#EACH data_sources.APPROX_LOCATION}} + - `{{ITEM}}` + {{/EACH}} + {{/IF}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.LOCATION}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Scope | Target SDK | Finding / Condition Checked | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **Foreground** | Any | Requests `ACCESS_FINE_LOCATION` but features only require city-level or approximate weather/search features. | `IMPORTANT` | Downgrade Manifest to `ACCESS_COARSE_LOCATION` to respect the minimum scope mandate. | + | **Foreground** | Any | App purpose (`{{APP_NAME}}`) does not imply location, yet foreground tracking is used, and prominent disclosure alert code is missing. | `IMPORTANT` | Implement a **Prominent In-App Disclosure** dialog explaining what location data is collected *before* requesting foreground permission. | + | **Background** | Any | Requests `ACCESS_BACKGROUND_LOCATION` solely for advertising, marketing, or general analytics. | `CRITICAL` | **High-Risk Violation**: Completely remove background location collection from the codebase. | + | **Background** | Any | Requests background location, but features could operate with foreground location access. | `IMPORTANT` | Downgrade the feature to foreground-only location tracking and remove background permission. | + | **Background** | Any | Background location is legitimate, but prominent disclosure does not mention "location" and "when the app is closed or not in use." | `IMPORTANT` | Update the Prominent Disclosure text to explicitly state "location" and "when closed or not in use". | + | **Foreground** | **37 or higher** | Precise location is requested on Android 17+. | `SUGGESTION` | Migrate to the **Location Button** API as the minimum scope mechanism for precise foreground location. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Actively trace custom background workers and disclosures: + 1. **The Foreground Sufficiency Test**: Analyze background threads, + `WorkManager` tasks, or background services triggering location updates. If + the background process performs syncing or location calculations that could + be deferred to when the user is actively viewing the app, flag an + `IMPORTANT` violation. + 2. **The Reasonable Expectation Test**: If the app label (`{{APP_NAME}}`) or + packages suggest a utility that shouldn't logically track location, + evaluate any indirect geo-tracking (such as geo-lookup of network IP + addresses, or sending local Wi-Fi SSID logs off-device). If found, flag an + `IMPORTANT` missing prominent disclosure. +{{/IF_ANY}} +{{/IF}} + +{{#IF_ALL requested_permissions.CONTACTS, data_sources.CONTACTS}} +#### Contacts Access Policy (Policy ID: contacts_access_policy) + +- **Goal**: Evaluate if broad contacts access (`READ_CONTACTS`) is justified or + if the Android Contact Picker should be used. +- **The Policy Spirit**: Address books contain sensitive personal details. The + Play Store mandates that apps use scoped contact access unless broad, + continuous contacts synchronization is critical (such as in social network + friend-matching or full contact managers). +- **Evidence**: + {{#EACH data_sources.CONTACTS}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.CONTACTS}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Target SDK | Access Model | Condition / Finding Checked | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **Any** | Broad (`READ_CONTACTS`) | Broad access is requested for one-time transactions, file sharing, referrals, or simple forms. | `IMPORTANT` | Migrate to the **Android Contact Picker** (`Intent.ACTION_PICK_CONTACTS`) to query contacts securely. | + | **37 or higher** | Broad (`READ_CONTACTS`) | Target SDK is 37+ (effective Oct 2026), and broad access is used for secondary or standard tasks. | `IMPORTANT` | Migrate to the **Android Contact Picker** as broad contacts access is restricted. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Evaluate the depth of contact queries: + 1. **Single-Item Verification**: Search for database cursors reading contact + tables (`ContactsContract.CommonDataKinds`). If the cursors are used solely + to let a user select a single friend, mobile phone number, or email + address, broad contacts access is a violation. Recommend Contact Picker. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.exact_alarm, exact_alarm}} +#### Exact Alarm Policy (Policy ID: exact_alarm_policy) + +- **Goal**: Evaluate if the app's core functionality justifies the + `USE_EXACT_ALARM` permission. +- **The Policy Spirit**: Exact alarms degrade system performance and battery + life. The Play Store strictly limits the high-risk `USE_EXACT_ALARM` + permission to alarm clocks, timers, and calendar apps where precise, + down-to-the-second timing is critical. +- **Evidence**: + {{#EACH exact_alarm}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.exact_alarm}} + - `{{ITEM}}` + {{/EACH}} +- **Common Evaluation Matrix**: + | Core App Purpose | Permission Requested | Justified? | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **Alarm Clock, Timer, or Calendar App** | `USE_EXACT_ALARM` | Yes (Compliant) | None | No action needed. | + | **Standard Utility, Game, Sync, or Productivity App** | `USE_EXACT_ALARM` | No (Violation) | `IMPORTANT` | Switch to `SCHEDULE_EXACT_ALARM` which respects system battery constraints, or use standard `AlarmManager` inexact scheduling. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Examine alarm trigger targets: + 1. **Time-Insensitive Syncing**: Verify the files scheduling alarms. If alarms + are used to fetch network updates, clean cache files, trigger analytics + uploads, or post local daily notifications, exact timing is not justified. + Recommend using `WorkManager` for background tasks instead of + `AlarmManager`. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.foreground_service, foreground_service}} +#### Foreground Services (Policy ID: foreground_services_policy) + +- **Goal**: Verify the declaration and justification of foreground services. +- **The Policy Spirit**: Foreground services keep processes alive in the + background and must be highly visible to users. Every declared service must + have an appropriate `foregroundServiceType` defined in the Manifest, and + special types like `specialUse` require specific tag property justifications. +- **Evidence**: + {{#EACH foreground_service}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.foreground_service}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Service Configuration | Justification Check | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **Missing type tag** | Foreground service is declared but lacks a `foregroundServiceType` attribute. | `CRITICAL` | Add the appropriate `android:foregroundServiceType` attribute to the service declaration in the Manifest. | + | **Lacks specialUse property** | Service type is `specialUse`, but Manifest lacks the required `` tag. | `CRITICAL` | Add the `` tag inside the service block with a valid subtype string. | + | **Type Misalignment** | Declared FGS type does not logically align with the app's core purpose. | `IMPORTANT` | Re-align FGS type to match app features, or migrate background operations to **WorkManager** if user-visible foreground presence is not justified. | + | **Declaration Reminder** | Foreground service is declared (even if type is correct). | `SUGGESTION` | **Play Console Declaration Required**: For apps targeting Android 14+, you must complete a Foreground Service declaration in the Play Console (App content section) for each type used, providing a functional description, user impact video, and a specific use case selection. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Critique specialUse justifications and service behavior: + 1. **Justification String Audit**: Read the text of the `` tag for + `specialUse`. If the text contains weak, boilerplate, or placeholder + justifications (e.g., "requires background process for app to run"), flag + an `IMPORTANT` violation warning the developer that Google Play reviewers + will reject this service. + 2. **Notification Integrity**: Verify if the FGS implementation creates a + valid user-facing notification. If no `startForeground()` or notification + builder logic is associated with the service initiation, flag an + `IMPORTANT` violation. + 3. **Play Console Declaration Confirmation**: If any foreground service is + used, flag a `SUGGESTION` to remind the developer that a specialized + declaration form in the Play Console is mandatory, requiring a video + demonstration of the feature. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.accessibility, accessibility}} +#### Accessibility API Policy (Policy ID: accessibility_api_policy) + +- **Goal**: Verify the configuration and justification of Accessibility + Services. +- **The Policy Spirit**: Accessibility APIs provide deep system access to assist + users with disabilities. Using these APIs for non-accessibility tasks (like UI + automation, screen scraping, background monitoring, or ad blocking) is + strictly prohibited by Google Play and causes immediate app rejection. +- **Evidence**: + {{#EACH accessibility}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.accessibility}} + - `{{ITEM}}` + {{/EACH}} +- **Common Evaluation Matrix**: + | Service Configuration | Real Code Usage | Justified? | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **`isAccessibilityTool="true"`** | Code performs screen scraping, ad blocking, or automated click routines for standard users. | No (Violation) | `CRITICAL` | **Immediate Rejection Risk**: Remove accessibility helper configs. Migrate UI automation to standard Android testing libraries. | + | **`isAccessibilityTool` false/missing** | Code implements an accessibility listener but is not a dedicated helper app. | No prominent disclosure | `CRITICAL` | **Immediate Rejection Risk**: Because you request accessibility permission for a standard utility, you MUST implement an in-app **Prominent Disclosure and Affirmative Consent screen** before asking the user to enable the service, otherwise your app will be rejected. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Trace the ingestion of accessibility events: + 1. **Telemetry Siphoning**: Scan the Accessibility Service methods + (`onAccessibilityEvent`). If the service captures on-screen text, + notifications, or keystrokes and routes them to local caches, shared + preferences, or off-device network logging endpoints, flag a `CRITICAL` + violation under the Accessibility API policy. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.APPS_ON_DEVICE, data_sources.APPS_ON_DEVICE}} +#### Package Visibility (Policy ID: package_visibility_policy) + +- **Goal**: Evaluate if the app's core functionality justifies broad visibility + into installed apps (`QUERY_ALL_PACKAGES` or related package APIs). +- **The Policy Spirit**: The list of installed apps reveals sensitive user + habits. The Play Store strictly restricts the `QUERY_ALL_PACKAGES` permission + to apps that directly manage device safety (Antivirus, File Managers, Device + Search). +- **Evidence**: + {{#EACH data_sources.APPS_ON_DEVICE}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.APPS_ON_DEVICE}} + - `{{ITEM}}` + {{/EACH}} +- **Common Evaluation Matrix**: + | Core App Purpose | Permission Requested | Justified? | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **Antivirus, File Manager, or Device Search** | `QUERY_ALL_PACKAGES` | Yes (Compliant) | None | No action needed. | + | **Any App** | `QUERY_ALL_PACKAGES` | No, used for ads, analytics, or secondary marketing. | `IMPORTANT` | **High-Risk Violation**: Remove the permission from the Manifest. Use specific `` intent declarations if package checks are strictly required for sharing/integration. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Examine indirect queries: + 1. **Indirect Package Searching**: Check if the code queries package details + dynamically using `PackageManager.getInstalledPackages()` or checks intents + in loops. If the intent lists are used to deduce if competitor apps or + advertising profiles exist, flag an `IMPORTANT` violation of package + visibility guidelines. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.SMS_CALL_LOG, data_sources.SMS_CALL_LOG}} +#### SMS and Call Log Permissions (Policy ID: sms_call_log_policy) + +- **Goal**: Evaluate if the app's core functionality justifies access to + sensitive SMS or Call Log data. +- **The Policy Spirit**: SMS and Call details are high-risk. Google Play + restricts access to default phone handlers and default SMS handlers. Standard + utility or shopping apps must use non-privileged APIs for SMS-based + verification. +- **Evidence**: + {{#EACH data_sources.SMS_CALL_LOG}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.SMS_CALL_LOG}} + - `{{ITEM}}` + {{/EACH}} +- **Common Evaluation Matrix**: + | Code Context | Finding / Trigger Checked | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **OTP/Account Verification** | App requests SMS permissions to automatically read login verification codes (OTPs). | `IMPORTANT` | Migrate to the **SMS Retriever API** or **SMS User Consent API** which require zero permissions. | + | **Utility or Secondary Feature** | App requests SMS/Call log access for non-core dashboard or notification features. | `CRITICAL` | **High-Risk Violation**: Completely remove SMS and Call Log permissions from the Manifest. | + | **Default SMS Handler** | App appears to be a legitimate handler (e.g. implements required intent filters). | `SUGGESTION` | **Play Console Declaration**: You must submit a **Permissions Declaration Form** in the Play Console and may be required to provide a video demonstration of this core functionality. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Audit background receivers: + 1. **Background Incoming SMS Listeners**: Check if a `BroadcastReceiver` + listens to `android.provider.Telephony.SMS_RECEIVED`. If this receiver + parses text in the background without being a designated default SMS + handler, flag a `CRITICAL` violation. + 2. **Default Handler Confirmation**: If the app requests SMS/Call Log + permissions and correctly implements the system-mandated intent filters for + a Default Handler, flag a `SUGGESTION` to remind the developer about the + mandatory Play Console declaration form. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.AUDIO, data_sources.AUDIO}} +#### Audio Recording Policy (Policy ID: audio_recording_policy) + +- **Goal**: Evaluate if the app's core functionality justifies broad access to + audio recording. +- **The Policy Spirit**: Unprompted audio recording is a severe privacy breach. + Apps should request microphone access strictly for user-visible, time-bounded + actions. Target SDK 34+ encourages using the system-managed Microphone Button + for temporary needs. +- **Evidence**: + {{#EACH data_sources.AUDIO}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.AUDIO}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Target SDK | Audio Recording Trigger Context | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **34 or higher** | App requests broad `RECORD_AUDIO` permission for occasional, user-initiated vocal input or short recording. | `IMPORTANT` | Migrate to the **Android Microphone Button** API to process temporary audio securely. | + | **Any** | App captures audio for secondary features (analytics, user-agent details, etc.) without explicit user control. | `IMPORTANT` | Remove microphone permissions. For general searches, integrate standard Android Speech Recognizer intents. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Verify recording indicators and threads: + 1. **Continuous Capture**: Scan for active recorder threads (`AudioRecord` or + `MediaRecorder`). If recording loops can be active when the app is + minimized or without a visible user-facing indicator, flag this as a + `CRITICAL` violation. +{{/IF_ALL}} + +{{#IF_ALL requested_permissions.FILES_AND_DOCS, data_sources.FILES_AND_DOCS}} +#### Files and Docs Access Policy (Policy ID: files_and_docs_policy) + +- **Goal**: Evaluate if broad file access (non-media) is justified or if the + Storage Access Framework should be used. +- **The Policy Spirit**: Storage isolation (Scoped Storage) is mandatory on + modern Android versions. Broad access to shared files is heavily restricted. + Standard files, documents, and download folders should be navigated using + scoped contracts to prevent global filesystem snooping. +- **Evidence**: + {{#EACH data_sources.FILES_AND_DOCS}} + - `{{ITEM}}` + {{/EACH}} + **Relevant Permissions Requested**: + {{#EACH requested_permissions.FILES_AND_DOCS}} + - `{{ITEM}}` + {{/EACH}} + {{#IF TARGET_SDK}} + **Target SDK**: `{{TARGET_SDK}}` + {{/IF}} +- **Common Evaluation Matrix**: + | Target SDK | Storage Configuration | Justified? | Severity | Direct Actionable Recommendation | + | :--- | :--- | :--- | :--- | :--- | + | **30 or higher** | App requests broad `READ_EXTERNAL_STORAGE` or `WRITE_EXTERNAL_STORAGE` for simple document selection. | No (Violation) | `IMPORTANT` | **Scoped Storage Mandate**: Migrate your document/file selections to the **Storage Access Framework (SAF)** (`Intent.ACTION_OPEN_DOCUMENT`). | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Critique external directory creation: + 1. **Manual File Sync Heuristic**: Check if the code creates custom root-level + folders on external storage (e.g. + `Environment.getExternalStorageDirectory() + "/my_folder"`). If directories + are created for standard document outputs or logging, flag an `IMPORTANT` + violation. Direct the developer to utilize scoped storage paths. +{{/IF_ALL}} + +## Output schema + +Save final JSON output to `{{TEMP_DIR}}/worker_{{GOAL_NAME}}.json`. + +```json +{ + "domain": "Permissions and APIs", + "findings": [ + { + "policy_id": "STRING_VALUE (The exact Policy ID, e.g., photo_video_access_policy)", + "issue_summary": "STRING_VALUE", + "severity": "CRITICAL | IMPORTANT | SUGGESTION", + "files_involved": ["STRING_VALUE"], + "evidence": "STRING_VALUE", + "recommendation": "STRING_VALUE" + } + ] +} +``` diff --git a/play/play-policy-insights/resources/goal_user_account.md b/play/play-policy-insights/resources/goal_user_account.md new file mode 100644 index 0000000..336e58d --- /dev/null +++ b/play/play-policy-insights/resources/goal_user_account.md @@ -0,0 +1,106 @@ +{{#IF_ANY data_sources.USER_ACCOUNT, data_sources.ACCOUNT_DELETION}} +{{ACTIVATE_GOAL}} +{{/IF_ANY}} +## User Account and Identity Audit + +### Hint: Deducing Account Presence + +To determine if an app handles user accounts (even if it uses third-party +providers like Google Sign-In or Firebase Auth), look for: +1. **Login/Auth Screens**: Semantic files or layouts named `login`, `auth`, + `signin`, or `signup`. +2. **Account Management**: APIs like `AccountManager`, `CredentialManager`, or + `Firebase.auth`. +3. **User Profile**: UI strings or data models referencing `profile`, + `my account`, or `user settings`. + +### Policies to Verify + +{{#IF_ANY data_sources.USER_ACCOUNT, semantic_files.USER_ACCOUNT}} +#### Play Console Requirements (Policy ID: login_credentials) + +- **Goal**: Identify if the app implements a login wall or authentication + screen. +- **The Policy Spirit**: To ensure Play Store reviewers can successfully test + and audit apps, developers must submit functional, non-expiring credentials in + Play Console if the app's features are gated behind a login screen. +- **Evidence**: + {{#IF semantic_files.USER_ACCOUNT}} + - **Account Files**: + `{{#EACH semantic_files.USER_ACCOUNT}}{{ITEM}}, {{/EACH}}` + {{/IF}} + {{#IF data_sources.USER_ACCOUNT}} + - **Account Signals**: + `{{#EACH data_sources.USER_ACCOUNT}}{{ITEM}}, {{/EACH}}` + {{/IF}} +- **Common Evaluation Matrix**: + | App State | Finding / Condition | Severity | Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **Login Screen Detected** | App displays or contains a login screen, registration wall, or authentication interface. | `IMPORTANT` | **Administrative Console Requirements**: Because your app implements a login flow, you MUST complete two manual setup steps in the Play Console dashboard to pass review:
1. **Reviewer Demo Credentials**: Submit active, non-expiring test credentials so Google Play reviewers can access your gated features.
2. **Account Deletion Link**: Submit a public-facing web link for account deletion to satisfy Google Play's data deletion policies. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Look beyond standard native login buttons. Extrapolate using this heuristic: + 1. **Hidden Gatekeepers**: Analyze if certain critical features (e.g., + synchronizing local database, checkout forms, or member-only dashboards) + require authentication even if the app opens directly to a main page. If + you deduce that functional workflows require a login, treat it as a login + gate and output the `IMPORTANT` console credentials and deletion link + reminders. +{{/IF_ANY}} + +{{#IF_ANY data_sources.USER_ACCOUNT, data_sources.ACCOUNT_DELETION}} +#### Account Deletion Requirement (Policy ID: account_deletion) + +- **Goal**: If the app handles user accounts, it must provide a discoverable + in-app account deletion mechanism. +- **The Policy Spirit**: Users have a fundamental right to request data erasure. + If they can create an account in-app, they must be able to delete it in-app. + Deletion must wipe remote database records, not just sign out. +- **Evidence**: + {{#IF data_sources.USER_ACCOUNT}} + - **Account signals (Presence)**: + `{{#EACH data_sources.USER_ACCOUNT}}{{ITEM}}, {{/EACH}}` + {{/IF}} + {{#IF data_sources.ACCOUNT_DELETION}} + - **Deletion signals (Mechanism)**: + `{{#EACH data_sources.ACCOUNT_DELETION}}{{ITEM}}, {{/EACH}}` + {{/IF}} +- **Common Evaluation Matrix**: + | App Account Status | Finding / Deletion Evidence | Severity | Actionable Recommendation | + | :--- | :--- | :--- | :--- | + | **Handles User Accounts** | App manages user accounts, but NO code, layout, or string suggests an in-app deletion button or process. | `IMPORTANT` | **Implement in-app deletion**: Create a highly discoverable path (e.g., under User Profile/Account Settings) to let users initiate account deletion directly in the app. | + +- **Domain-Specific Heuristics (Strictly Bounded)**: + Do not limit your analysis to basic "Delete" buttons. Actively analyze custom + and grey-area implementations: + 1. **The Partial Deletion Trap**: Read the implementation of any found + deletion mechanisms. If the code merely calls `clearPreferences()`, clears + a local cookie, or triggers a standard `logout()` without sending a remote + delete/purge network API call to clean up backend user records, flag this + as a `IMPORTANT` violation of the deletion mandate. + 2. **Indirect User Accounts**: If the app uses third-party sign-in bridges + (e.g., Google Sign-In, Firebase) but does not store an + explicit account profile on its own server, it still handles user account + details if any user preferences or device identifiers are cached remotely. + If so, a delete link/button is still required. +{{/IF_ANY}} + +## Output schema + +Save final JSON output to `{{TEMP_DIR}}/worker_{{GOAL_NAME}}.json`. + +```json +{ + "domain": "User Account and Identity", + "findings": [ + { + "policy_id": "STRING_VALUE (The exact Policy ID, e.g., account_deletion)", + "issue_summary": "STRING_VALUE", + "severity": "CRITICAL | IMPORTANT | SUGGESTION", + "files_involved": ["STRING_VALUE"], + "evidence": "STRING_VALUE", + "recommendation": "STRING_VALUE" + } + ] +} +``` diff --git a/play/play-policy-insights/resources/policies.json b/play/play-policy-insights/resources/policies.json new file mode 100644 index 0000000..d069a83 --- /dev/null +++ b/play/play-policy-insights/resources/policies.json @@ -0,0 +1,321 @@ +{ + "data_safety_section": { + "name": "Data safety section", + "category": "User Data", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/10144311#safetysection", + "https://developer.android.com/privacy-and-security/declare-data-use", + "https://support.google.com/googleplay/android-developer/answer/10787469" + ], + "taxonomy": { + "PRECISE_LOCATION": { + "category": "Location", + "data_type": "Precise location", + "description": "User or device physical location within an area less than 3 square kilometers, such as location provided by Android’s ACCESS_FINE_LOCATION permission." + }, + "APPROX_LOCATION": { + "category": "Location", + "data_type": "Approximate location", + "description": "User or device physical location to an area greater than or equal to 3 square kilometers, such as the city a user is in, or location provided by Android’s ACCESS_COARSE_LOCATION permission." + }, + "USER_ACCOUNT": { + "category": "Personal info", + "data_type": "User IDs", + "description": "Identifiers that relate to an identifiable person. For example, an account ID, account number, or account name." + }, + "NAME": { + "category": "Personal info", + "data_type": "Name", + "description": "How a user refers to themselves, such as their first or last name, or nickname." + }, + "EMAIL": { + "category": "Personal info", + "data_type": "Email address", + "description": "A user’s email address." + }, + "ADDRESS": { + "category": "Personal info", + "data_type": "Address", + "description": "A user’s address, such as a mailing or home address." + }, + "PHONE": { + "category": "Personal info", + "data_type": "Phone number", + "description": "A user’s phone number." + }, + "RACE_ETHNICITY": { + "category": "Personal info", + "data_type": "Race and ethnicity", + "description": "Information about a user’s race or ethnicity." + }, + "POLITICAL_RELIGIOUS_BELIEFS": { + "category": "Personal info", + "data_type": "Political or religious beliefs", + "description": "Information about a user’s political or religious beliefs." + }, + "SEXUAL_ORIENTATION": { + "category": "Personal info", + "data_type": "Sexual orientation", + "description": "Information about a user’s sexual orientation." + }, + "PERSONAL_INFO_OTHER": { + "category": "Personal info", + "data_type": "Other info", + "description": "Any other personal information such as date of birth, gender identity, veteran status, etc." + }, + "CREDIT_DEBIT_BANK_ACCOUNT_NUMBER": { + "category": "Financial info", + "data_type": "User payment info", + "description": "Information about a user’s financial accounts such as credit card number." + }, + "PURCHASE_HISTORY": { + "category": "Financial info", + "data_type": "Purchase history", + "description": "Information about purchases or transactions a user has made." + }, + "CREDIT_SCORE": { + "category": "Financial info", + "data_type": "Credit score", + "description": "Information about a user’s credit score." + }, + "FINANCIAL_INFO_OTHER": { + "category": "Financial info", + "data_type": "Other financial info", + "description": "Any other financial information such as user salary or debts." + }, + "HEALTH": { + "category": "Health and fitness", + "data_type": "Health info", + "description": "Information about a user's health, such as medical records or symptoms." + }, + "FITNESS": { + "category": "Health and fitness", + "data_type": "Fitness info", + "description": "Information about a user's fitness, such as exercise or other physical activity." + }, + "EMAILS": { + "category": "Messages", + "data_type": "Emails", + "description": "A user’s emails including the email subject line, sender, recipients, and the content of the email." + }, + "SMS_CALL_LOG": { + "category": "Messages", + "data_type": "SMS or MMS", + "description": "A user’s text messages including the sender, recipients, and the content of the message." + }, + "OTHER_MESSAGES": { + "category": "Messages", + "data_type": "Other in-app messages", + "description": "Any other types of messages. For example, instant messages or chat content." + }, + "PHOTOS": { + "category": "Photos and videos", + "data_type": "Photos", + "description": "A user’s photos." + }, + "VIDEOS": { + "category": "Photos and videos", + "data_type": "Videos", + "description": "A user’s videos." + }, + "AUDIO": { + "category": "Audio files", + "data_type": "Voice or sound recordings", + "description": "A user’s voice such as a voicemail or a sound recording." + }, + "MUSIC": { + "category": "Audio files", + "data_type": "Music files", + "description": "A user’s music files." + }, + "OTHER_AUDIO": { + "category": "Audio files", + "data_type": "Other audio files", + "description": "Any other user-created or user-provided audio files." + }, + "FILES_AND_DOCS": { + "category": "Files and docs", + "data_type": "Files and docs", + "description": "A user’s files or documents, or information about their files or documents such as file names." + }, + "CALENDAR": { + "category": "Calendar", + "data_type": "Calendar events", + "description": "Information from a user’s calendar such as events, event notes, and attendees." + }, + "CONTACTS": { + "category": "Contacts", + "data_type": "Contacts", + "description": "Information about the user’s contacts such as contact names, message history, and social graph information like usernames, contact recency, contact frequency, interaction duration and call history." + }, + "APP_INTERACTIONS": { + "category": "App activity", + "data_type": "App interactions", + "description": "Information about how a user interacts with the app. For example, the number of times they visit a page or sections they tap on." + }, + "IN_APP_SEARCH_HISTORY": { + "category": "App activity", + "data_type": "In-app search history", + "description": "Information about what a user has searched for in your app." + }, + "APPS_ON_DEVICE": { + "category": "App activity", + "data_type": "Installed apps", + "description": "Information about the apps installed on a user's device." + }, + "USER_GENERATED_CONTENT": { + "category": "App activity", + "data_type": "Other user-generated content", + "description": "Any other user-generated content not listed here, or in any other section. For example, user bios, notes, or open-ended responses." + }, + "OTHER_ACTIONS": { + "category": "App activity", + "data_type": "Other actions", + "description": "Any other user activity or actions in-app not listed here such as gameplay, likes, and dialog options." + }, + "WEB_BROWSING_HISTORY": { + "category": "Web browsing", + "data_type": "Web browsing history", + "description": "Information about the websites a user has visited." + }, + "CRASH_LOGS": { + "category": "App info and performance", + "data_type": "Crash logs", + "description": "Crash log data from your app. For example, the number of times your app has crashed, stack traces, or other information directly related to a crash." + }, + "PERFORMANCE_DIAGNOSTICS": { + "category": "App info and performance", + "data_type": "Diagnostics", + "description": "Information about the performance of your app. For example battery life, loading time, latency, framerate, or any technical diagnostics." + }, + "OTHER_APP_PERFORMANCE": { + "category": "App info and performance", + "data_type": "Other app performance data", + "description": "Any other app performance data excluding crash log data (e.g., stack traces) and diagnostics (e.g., battery life, loading time, latency, or framerate)." + }, + "DEVICE_ID": { + "category": "Device or other IDs", + "data_type": "Device or other IDs", + "description": "Identifiers that relate to an individual device, browser or app. For example, an IMEI number, MAC address, Widevine Device ID, Firebase installation ID, or advertising identifier." + } + } + }, + "login_credentials": { + "name": "Login credentials", + "category": "Play Console Requirements", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/15748846" + ] + }, + "sms_call_log_policy": { + "name": "SMS and Call Log Permissions", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#sms-call-permissions", + "https://support.google.com/googleplay/android-developer/answer/10208820" + ] + }, + "package_visibility_policy": { + "name": "Package Visibility", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#package-vis", + "https://support.google.com/googleplay/android-developer/answer/10158779" + ] + }, + "all_files_access_policy": { + "name": "All Files Access", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#all-files-access", + "https://developer.android.com/training/data-storage/manage-all-files", + "https://support.google.com/googleplay/android-developer/answer/10467955" + ] + }, + "photo_video_access_policy": { + "name": "Photo and Video Permissions", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#photo-and-video-permissions", + "https://support.google.com/googleplay/android-developer/answer/16935362", + "https://developer.android.com/training/data-storage/shared/media" + ] + }, + "location_access_policy": { + "name": "Location Permissions", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241", + "https://support.google.com/googleplay/android-developer/answer/9799150" + ] + }, + "contacts_access_policy": { + "name": "Contacts Permissions", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16909972#contacts-permissions", + "https://support.google.com/googleplay/android-developer/answer/16935362" + ] + }, + "audio_recording_policy": { + "name": "Audio Recording Policy", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/10144311", + "https://support.google.com/googleplay/android-developer/answer/16558241" + ] + }, + "files_and_docs_policy": { + "name": "Files and Docs Access Policy", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/10467955", + "https://support.google.com/googleplay/android-developer/answer/16558241" + ] + }, + "prominent_disclosure_policy": { + "name": "Prominent Disclosure Policy", + "category": "User Data", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/10144311" + ] + }, + "account_deletion": { + "name": "Account Deletion Requirement", + "category": "User Data", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/10144311#account_deletion", + "https://support.google.com/googleplay/android-developer/answer/13327111" + ] + }, + "target_api_level": { + "name": "Target API Level Requirements", + "category": "Privacy, Deception and Device Abuse", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16561298", + "https://support.google.com/googleplay/android-developer/answer/11926878" + ] + }, + "accessibility_api_policy": { + "name": "Accessibility API Policy", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#accessibility" + ] + }, + "exact_alarm_policy": { + "name": "Exact Alarm Policy", + "category": "Permissions", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/16558241#exact_alarm" + ] + }, + "foreground_services_policy": { + "name": "Foreground Services", + "category": "Privacy, Deception and Device Abuse", + "urls": [ + "https://support.google.com/googleplay/android-developer/answer/13392821", + "https://support.google.com/googleplay/android-developer/answer/16559646#foreground_service" + ] + } +} diff --git a/play/play-policy-insights/resources/scanner_config.json b/play/play-policy-insights/resources/scanner_config.json new file mode 100644 index 0000000..6126957 --- /dev/null +++ b/play/play-policy-insights/resources/scanner_config.json @@ -0,0 +1,799 @@ +{ + "signal_categories": { + "data_sources": { + "PRECISE_LOCATION": [ + "ACCESS_FINE_LOCATION", + "FusedLocationProviderClient", + "requestSingleUpdate", + "getLastKnownLocation", + "navigator.geolocation", + "Geolocator", + "location.getLocation()", + "latitude", + "longitude", + "gps_coords", + "watchPosition", + "getCurrentPosition", + "getPositionStream", + "LocationAccuracy.high", + "react-native-geolocation-service", + "Expo.Location" + ], + "APPROX_LOCATION": [ + "ACCESS_COARSE_LOCATION", + "ACCESS_BACKGROUND_LOCATION", + "LocationManager", + "coarse_location", + "ip_location", + "ip_country", + "LocationAccuracy.low", + "LocationAccuracy.balanced" + ], + "USER_ACCOUNT": [ + "AccountManager", + "registerUser", + "login", + "signIn", + "signUp", + "createAccount", + "GET_ACCOUNTS", + "isLoggedIn", + "sign in", + "firebaseAuth", + "CredentialManager", + "credentials", + "auth_token", + "session_token", + "user_id", + "uid", + "FirebaseAuth", + "GoogleSignIn", + "SignInWithApple", + "flutter_facebook_auth", + "Auth0", + "@react-native-firebase/auth", + "react-native-google-signin", + "react-native-app-auth" + ], + "ACCOUNT_DELETION": [ + "deleteAccount", + "purgeUserData", + "closeAccount", + "removeUser", + "deactivate", + "requestDelete", + "delete_profile", + "destroy_account" + ], + "NAME": [ + "textPersonName", + "user_name", + "firstName", + "lastName", + "display_name", + "full_name", + "fullName", + "real_name", + "realName", + "family_name", + "familyName", + "given_name", + "contact_name", + "profile_name", + "NAME_DATA_EXTRA", + "middleName", + "nickname", + "TextInputType.name" + ], + "EMAIL": [ + "textEmailAddress", + "email_address", + "user_email", + ".email", + "emailAddress", + "userEmail", + "accountEmail", + "senderEmail", + "recipientEmail", + "mailAddress", + "getEmail(", + "setEmail(", + "email_id", + "primary_email", + "keyboardType=\"email-address\"", + "TextInputType.emailAddress" + ], + "ADDRESS": [ + "textPostalAddress", + "postal_address", + "user_address", + "street_address", + "billing_address", + "shipping_address", + "postal_code", + "zip_code", + "country_code", + "autoComplete=\"postal-address\"", + "TextInputType.streetAddress" + ], + "PHONE": [ + "textPhone", + "phone_number", + "user_phone", + "getLine1Number", + "READ_PHONE_NUMBERS", + "mobile_phone", + "telephone", + "phone_id", + "textContentType=\"telephoneNumber\"", + "TextInputType.phone" + ], + "RACE_ETHNICITY": [ + "race", + "ethnicity", + "tribal", + "demographics" + ], + "POLITICAL_RELIGIOUS_BELIEFS": [ + "religion", + "religious", + "political_affiliation", + "political_party", + "political_beliefs" + ], + "SEXUAL_ORIENTATION": [ + "sexual_orientation", + "gender_identity", + "gender_preference" + ], + "PERSONAL_INFO_OTHER": [ + "date_of_birth", + "dob", + "birth_date", + "gender", + "sex", + "salutation" + ], + "CREDIT_DEBIT_BANK_ACCOUNT_NUMBER": [ + "cardNumber", + "card_number", + "cvv", + "expiry_date", + "stripe", + "paypal", + "bank_account", + "iban", + "swift_code", + "credit_card", + "card_holder", + "routing_number", + "flutter_stripe", + "stripe-react-native", + "initPaymentSheet" + ], + "PURCHASE_HISTORY": [ + "purchaseHistory", + "transactionHistory", + "orderHistory", + "orderId", + "purchaseToken", + "queryPurchases", + "queryPurchasesAsync", + "startPurchaseFlow", + "PurchasesUpdatedListener", + "BillingClient", + "receipt", + "invoice", + "payment_history", + "in_app_purchase", + "react-native-iap", + "RNIap" + ], + "CREDIT_SCORE": [ + "credit_score", + "fico", + "credit_rating", + "equifax", + "experian", + "transunion" + ], + "FINANCIAL_INFO_OTHER": [ + "salary", + "income", + "net_worth", + "outstanding_debt", + "user_debt" + ], + "HEALTH": [ + "BODY_SENSORS", + "BODY_SENSORS_BACKGROUND", + "SensorManager", + "Health Connect", + "medical_record", + "symptoms", + "blood_pressure", + "glucose_level", + "HealthFactory", + "HealthDataType", + "getHealthDataFromTypes", + "HealthConnectClient", + "AppleHealthKit" + ], + "FITNESS": [ + "ACTIVITY_RECOGNITION", + "Google Fit", + "HealthConnect", + "Sleep API", + "step_counter", + "HeartRate", + "Heart_Rate", + "Cadence", + "PowerMeter", + "CyclingPower", + "CaloriesBurned", + "Workout", + "PhysicalActivity", + "GPX", + "ANT+", + "react-native-google-fit", + "GoogleFit", + "fitbit" + ], + "EMAILS": [ + "javax.mail", + "smtp", + "imap", + "pop3", + "message/rfc822", + "mailserver", + "email_body", + "sendMail", + "mail_subject", + "email_attachment" + ], + "SMS_CALL_LOG": [ + "READ_SMS", + "RECEIVE_SMS", + "SEND_SMS", + "READ_CALL_LOG", + "WRITE_CALL_LOG", + "SmsRetriever" + ], + "OTHER_MESSAGES": [ + "chat_message", + "instant_message", + "xmpp", + "matrix_sdk", + "chat_history", + "direct_message", + "send_message", + "websocket_chat" + ], + "MEDIA": [ + "READ_MEDIA_IMAGES", + "READ_MEDIA_VIDEO", + "PhotoPicker", + "MediaStore", + "ACTION_GET_CONTENT", + "ACTION_OPEN_DOCUMENT", + "image/*", + "video/*" + ], + "PHOTOS": [ + "READ_MEDIA_IMAGES", + "READ_MEDIA_VISUAL_USER_SELECTED", + "PhotoPicker", + "PickVisualMedia", + "MediaStore.Images", + "ACTION_PICK", + "ACTION_GET_CONTENT", + "image/*", + "TakePicture", + "ImageCapture", + "CAMERA", + "image_picker", + "launchImageLibrary", + "launchCamera", + "react-native-image-picker", + "expo-image-picker", + "wechat_assets_picker" + ], + "VIDEOS": [ + "READ_MEDIA_VIDEO", + "READ_MEDIA_VISUAL_USER_SELECTED", + "MediaStore.Video", + "video/*", + "PickVisualMedia.VideoOnly", + "ACTION_VIDEO_CAPTURE", + "VideoCapture", + "MediaRecorder", + "ExoPlayer", + "MediaController", + "pickVideo", + "react-native-vision-camera", + "CameraRoll", + "expo-image-picker" + ], + "AUDIO": [ + "AudioRecord", + "MediaRecorder", + "RECORD_AUDIO", + "CAPTURE_AUDIO_OUTPUT", + "SpeechRecognizer", + "RecognitionListener", + "VoiceInteractionService", + "audio/*", + "audio/amr", + "audio/3gpp", + "voice_note", + "voice_memo", + "voicemail", + "dictation", + "voice_recorder", + "ambient_sound", + "record_button", + "mic_icon", + "record", + "Record.hasPermission", + "flutter_sound", + "FlutterSoundRecorder", + "react-native-audio-recorder-player", + "expo-av" + ], + "MUSIC": [ + "MediaPlayer", + "ExoPlayer", + "SimpleExoPlayer", + "AudioTrack", + "MediaController", + "MediaSession", + "PlaybackState", + "IS_MUSIC", + "MediaStore.Audio.Media", + "MediaStore.Audio.Playlists", + "MediaStore.Audio.Albums", + "MediaStore.Audio.Artists", + "audio/mpeg", + "audio/mp3", + "audio/flac", + "audio/x-wav", + "music", + "playlist", + "artist", + "album", + "track", + "song", + "mp3", + "flac", + "m4a", + "wma", + "audioplayers", + "assets_audio_player", + "react-native-track-player", + "react-native-sound" + ], + "OTHER_AUDIO": [ + "RingtoneManager", + "SoundPool", + "ACTUAL_DEFAULT_RINGTONE_URI", + "ACTION_RINGTONE_PICKER", + "IS_RINGTONE", + "IS_NOTIFICATION", + "IS_ALARM", + "IS_PODCAST", + "audio/ogg", + "audio/aac", + "audio/webm", + "ringtone", + "notification_sound", + "alarm_sound", + "podcast", + "sound_effect", + "audio_effect", + "sfx", + "soundboard" + ], + "FILES_AND_DOCS": [ + "READ_EXTERNAL_STORAGE", + "WRITE_EXTERNAL_STORAGE", + "MANAGE_EXTERNAL_STORAGE", + "*/*", + "localStorage", + "sessionStorage", + "AsyncStorage", + "shared_preferences", + "flutter_secure_storage", + "Hive", + "sqflite", + "application/pdf", + "application/msword", + "text/plain", + "path_provider", + "getApplicationDocumentsDirectory", + "getExternalStorageDirectory", + "getTemporaryDirectory", + "react-native-fs", + "RNFS", + "expo-file-system", + "MMKV", + "react-native-mmkv", + "Sembast", + "RxDB", + "Realm" + ], + "CALENDAR": [ + "READ_CALENDAR", + "WRITE_CALENDAR", + "CalendarContract", + "device_calendar", + "retrieveCalendars", + "react-native-calendar-events", + "RNCalendarEvents" + ], + "CONTACTS": [ + "READ_CONTACTS", + "WRITE_CONTACTS", + "ContactsContract", + "contacts_service", + "ContactsService.getContacts", + "flutter_contacts", + "FlutterContacts", + "react-native-contacts", + "Contacts.getAll" + ], + "APP_INTERACTIONS": [ + "logEvent", + "trackScreen", + "onItemClick", + "onClickListener", + "firebase.analytics()", + "Mixpanel", + "Amplitude", + "FirebaseAnalytics", + "AmplitudeFlutter", + "user_session", + "event_params", + "page_view", + "FirebaseAnalytics.instance", + "facebook_app_events", + "amplitude_flutter", + "mixpanel_flutter", + "@react-native-firebase/analytics", + "react-native-fbsdk-next", + "mixpanel-react-native", + "AppEventsLogger" + ], + "IN_APP_SEARCH_HISTORY": [ + "searchHistory", + "search_query", + "SearchRecentSuggestions", + "search_term", + "query_string" + ], + "APPS_ON_DEVICE": [ + "QUERY_ALL_PACKAGES", + "getPackageInfo", + "getInstalledPackages", + "queryIntentActivities" + ], + "USER_GENERATED_CONTENT": [ + "textMultiLine", + "edit_text", + "user_note", + "user_bio", + "user_feedback", + "comment_text" + ], + "WEB_BROWSING_HISTORY": [ + "WebView", + "loadUrl", + "WebSettings", + "setJavaScriptEnabled", + "browser_history", + "visited_url" + ], + "CRASH_LOGS": [ + "ApplicationErrorReport", + "Crashlytics", + "ACRA", + "Bugsnag", + "Sentry", + "uncaughtException", + "recordFlutterError", + "recordError", + "SentryFlutter", + "crashlytics()" + ], + "PERFORMANCE_DIAGNOSTICS": [ + "ActivityManager", + "BatteryManager", + "Choreographer", + "Debug", + "PowerManager", + "StrictMode", + "FirebasePerf", + "trace", + "api_latency", + "write_latency", + "query_time", + "elapsed_time", + "execution_time" + ], + "OTHER_APP_PERFORMANCE": [ + "WorkInfo", + "sync_status", + "sync_failed", + "api_error", + "request_timeout", + "status_code", + "getDatabasePath", + "db_size", + "database_size", + "Macrobenchmark", + "benchmark", + "telemetry", + "cpu_usage", + "memory_usage", + "heap_usage", + "bytes_sent", + "bytes_received", + "payload_size", + "heartbeat", + "foreground_duration", + "background_duration", + "cache_hit_rate", + "cache_size", + "queue_depth" + ], + "DEVICE_ID": [ + "TelephonyManager", + "getDeviceId", + "getSubscriberId", + "IMEI", + "IMSI", + "AndroidId", + "Settings.Secure.ANDROID_ID", + "AdvertisingIdClient", + "instanceId", + "firebase_token", + "push_token", + "fcm_token", + "device_info_plus", + "DeviceInfoPlugin", + "AndroidDeviceInfo", + "react-native-device-info", + "DeviceInfo", + "getUniqueId", + "getAndroidId", + "getAdvertisingId" + ], + "OTHER_ACTIONS": [ + "gameplay", + "button_click", + "option_selected", + "user_action" + ] + }, + "network_transmission": { + "-": [ + "fetch", + "axios", + "XMLHttpRequest", + "WebSocket", + "http.get", + "http.post", + "Dio", + "HttpClient" + ] + }, + "exact_alarm": { + "-": [ + "USE_EXACT_ALARM", + "SCHEDULE_EXACT_ALARM", + "setExact", + "setExactAndAllowWhileIdle", + "AlarmManager" + ] + }, + "accessibility": { + "-": [ + "AccessibilityService", + "AccessibilityEvent", + "BIND_ACCESSIBILITY_SERVICE" + ] + }, + "foreground_service": { + "-": [ + "FOREGROUND_SERVICE", + "startForeground", + "foregroundServiceType", + "ServiceInfo.FOREGROUND_SERVICE_TYPE", + "FOREGROUND_SERVICE_SPECIAL_USE", + "PROPERTY_SPECIAL_USE_FGS_SUBTYPE", + "specialUse" + ] + }, + "disclosure": { + "-": [ + "AlertDialog", + "Dialog", + "MaterialAlertDialogBuilder", + "Consent", + "Disclosure", + "Privacy Policy", + "Accept", + "Agree", + "collect", + "share", + "transmit", + "data safety", + "privacy" + ] + } + }, + "file_patterns": { + "USER_ACCOUNT": [ + "account", + "login", + "auth" + ], + "SUPPORT": [ + "support", + "help" + ], + "LEGAL": [ + "privacy", + "terms", + "legal" + ], + "CONFIG": [ + "config" + ], + "AUDIO": [ + "record", + "voice", + "mic" + ], + "MUSIC": [ + "music", + "playlist", + "song", + "player" + ], + "OTHER_AUDIO": [ + "ringtone", + "sound", + "effect", + "podcast", + "sfx" + ], + "OTHER_APP_PERFORMANCE": [ + "telemetry", + "benchmark", + "sync", + "performance" + ], + "PHOTOS": [ + "photo", + "image", + "picture", + "camera", + "avatar", + "gallery" + ], + "VIDEOS": [ + "video", + "movie", + "playback", + "camcorder", + "recording" + ] + }, + "permission_groups": { + "LOCATION": [ + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_BACKGROUND_LOCATION" + ], + "CONTACTS": [ + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS" + ], + "SMS_CALL_LOG": [ + "android.permission.READ_SMS", + "android.permission.RECEIVE_SMS", + "android.permission.SEND_SMS", + "android.permission.RECEIVE_MMS", + "android.permission.RECEIVE_WAP_PUSH", + "android.permission.READ_CALL_LOG", + "android.permission.WRITE_CALL_LOG", + "android.permission.PROCESS_OUTGOING_CALLS" + ], + "MEDIA": [ + "android.permission.READ_MEDIA_IMAGES", + "android.permission.READ_MEDIA_VIDEO", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + ], + "PHOTOS": [ + "android.permission.READ_MEDIA_IMAGES", + "android.permission.READ_MEDIA_VISUAL_USER_SELECTED", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.CAMERA" + ], + "VIDEOS": [ + "android.permission.READ_MEDIA_VIDEO", + "android.permission.READ_MEDIA_VISUAL_USER_SELECTED", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO" + ], + "ALL_FILES": [ + "android.permission.MANAGE_EXTERNAL_STORAGE" + ], + "AUDIO": [ + "android.permission.RECORD_AUDIO", + "android.permission.READ_MEDIA_AUDIO", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + ], + "MUSIC": [ + "android.permission.READ_MEDIA_AUDIO", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + ], + "OTHER_AUDIO": [ + "android.permission.READ_MEDIA_AUDIO", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + ], + "FILES_AND_DOCS": [ + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + ], + "APPS_ON_DEVICE": [ + "android.permission.QUERY_ALL_PACKAGES" + ], + "exact_alarm": [ + "android.permission.USE_EXACT_ALARM", + "android.permission.SCHEDULE_EXACT_ALARM" + ], + "foreground_service": [ + "android.permission.FOREGROUND_SERVICE" + ], + "accessibility": [ + "android.permission.BIND_ACCESSIBILITY_SERVICE" + ] + }, + "ignored_directories": [ + ".git", + ".scratch", + "build", + ".gradle", + ".idea", + "test", + "androidTest", + "tests", + "testFixtures", + "node_modules", + ".pub-cache", + "ios", + "web", + "macos", + "windows", + "linux" + ], + "supported_extensions": [ + ".java", + ".kt", + ".js", + ".ts", + ".jsx", + ".tsx", + ".dart", + ".cs", + ".vue" + ] +} diff --git a/play/play-policy-insights/scripts/generate_report.py b/play/play-policy-insights/scripts/generate_report.py new file mode 100755 index 0000000..6628216 --- /dev/null +++ b/play/play-policy-insights/scripts/generate_report.py @@ -0,0 +1,860 @@ +#!/usr/bin/env python3 + +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generates a comprehensive compliance report from synthesized findings.""" + +import argparse +from datetime import datetime +import glob +import json +import os +import re +import sys +import play_store_scraper +from template_engine import render_template + +# Precompiled regex patterns at module scope for performance optimizations +EMOJI_PATTERN = re.compile( + r"[\U00010000-\U0010ffff\u2600-\u27bf\u2300-\u23ff\u2b50]+", + flags=re.UNICODE, +) +CODE_LINK_PATTERN = re.compile(r"^(.*?)(?:[:\s]+L?(\d+))?$") +SEVERITY_CLEAN_PATTERN = re.compile(r"['\"`*🔴🟡🔵\s]+") + + +def run_scraper(package_name, output_dir): + """Invokes the Play Store scraper directly.""" + try: + print(f"Auditing {package_name} from Play Store...", file=sys.stderr) + result = play_store_scraper.scrape_app_details(package_name) + + # Save to temp for consistency with existing report logic if needed + os.makedirs(output_dir, exist_ok=True) + out_path = os.path.join(output_dir, "play_store_declaration.json") + with open(out_path, "w") as f: + json.dump(result, f, indent=4, sort_keys=True) + + return result + except Exception as e: + print(f"Warning: Play Store scrape failed: {e}", file=sys.stderr) + return {} + + +def strip_emojis(text): + """Removes emoji characters from a string.""" + if not isinstance(text, str): + return text + return EMOJI_PATTERN.sub(r"", text) + + +def format_code_link(file_val): + """Formats file paths and line numbers into Markdown links.""" + if not file_val or file_val == "N/A": + return "N/A" + files = [] + if isinstance(file_val, str): + if "," in file_val: + files = [f.strip() for f in file_val.split(",")] + else: + files = [file_val.strip()] + elif isinstance(file_val, list): + files = [str(f).strip() for f in file_val] + formatted_links = [] + for f in files: + f_str = str(f) + if f_str.startswith("["): + formatted_links.append(f_str) + continue + match = CODE_LINK_PATTERN.search(f_str) + if match: + path = match.group(1) + line = match.group(2) + basename = os.path.basename(path) + if line: + link = f"[{basename}:L{line}](file://{path}#L{line})" + else: + link = f"[{basename}](file://{path})" + formatted_links.append(link) + else: + formatted_links.append(f_str) + return ", ".join(formatted_links) + + +def render_table( + items, columns, column_names, empty_message="No items identified." +): + """Renders a Markdown table from a list of dictionaries.""" + if not items: + return f"\n* {empty_message}\n" + + widths = [len(name) for name in column_names] + rows = [] + for item in items: + row = [] + for col in columns: + val = str(item.get(col, "N/A")).replace("|", "\\|").replace("\n", "
") + row.append(val) + rows.append(row) + for i, val in enumerate(row): + widths[i] = max(widths[i], len(val)) + + def format_row(vals): + return ( + "| " + + " | ".join(val.ljust(widths[i]) for i, val in enumerate(vals)) + + " |" + ) + + header = format_row(column_names) + separator = ( + "| " + " | ".join("-" * widths[i] for i in range(len(widths))) + " |" + ) + body = "\n".join(format_row(row) for row in rows) + + return f"\n{header}\n{separator}\n{body}\n" + + +def parse_boolean(val): + """Safely converts boolean or sloppy string yes/no/true/false.""" + if isinstance(val, bool): + return val + if isinstance(val, str): + v_clean = val.strip().lower() + return v_clean in ("yes", "true", "1") + return False + + +def parse_disclosure_status(val): + """Safely parses disclosure enums or visual yes/no.""" + if not val: + return "No" + + val_clean = str(val).strip().upper() + if "EXEMPT" in val_clean: + return "Exempt (Obvious)" + if "DISCLOSED" in val_clean or "YES" in val_clean or "TRUE" in val_clean: + return "Yes" + + return "No" + + +def clean_severity(sev_str): + """Robustly cleans severity strings by removing quotes, asterisks, + emojis, and whitespace. + """ + if not isinstance(sev_str, str): + return "SUGGESTION" + + # Convert to uppercase and strip formatting characters + cleaned = sev_str.upper() + cleaned = SEVERITY_CLEAN_PATTERN.sub("", cleaned) + + if "CRITICAL" in cleaned: + return "CRITICAL" + if "IMPORTANT" in cleaned: + return "IMPORTANT" + if "SUGGESTION" in cleaned: + return "SUGGESTION" + + return "SUGGESTION" + + +def render_finding_card(finding, policy_refs=None): + """Renders a detailed finding card in Markdown.""" + severity = clean_severity(finding.get("severity")) + severity_map = { + "CRITICAL": "🔴 Critical", + "IMPORTANT": "🟡 Important", + "SUGGESTION": "🔵 Suggestion", + } + visual_severity = severity_map.get(severity, "🔵 Suggestion") + + title = strip_emojis( + finding.get("issue") or finding.get("issue_summary") or "Policy Risk" + ) + p_id = ( + finding.get("policy_id") + or finding.get("policy_reference") + or "Unknown Policy" + ) + policy_link = f"**{p_id}**" + + policy_info = policy_refs.get(p_id) if policy_refs else None + + policy_link = f"**{p_id}**" + policy_urls = [] + if p_id and policy_info: + policy_link = f"**{policy_info.get('name', p_id)}**" + policy_urls = policy_info.get("urls", []) + + files = finding.get("files_involved") + has_local_override = False + local_override_file = None + + if files and isinstance(files, list): + for f in files: + if "play_store_declaration.json" in f: + has_local_override = True + local_override_file = f + break + + files_formatted = format_code_link(files) + recommendation = finding.get("recommendation", "Review and remediate.") + + if has_local_override: + recommendation = ( + "Update the local Play Store declaration file at " + f"{format_code_link(local_override_file)}." + ) + + card = f"\n#### {title}\n" + card += f"- **Policy**: {policy_link}\n" + card += f"- **Severity**: {visual_severity}\n" + card += f"- **Files**: {files_formatted}\n" + card += f"- **Evidence**: {finding.get('evidence', 'N/A')}\n" + card += f"- **Recommendation**: {recommendation}\n" + + if policy_urls: + card += "- **References**:\n" + for url in policy_urls: + card += f" - {url}\n" + + return card + + +def load_json(file_path): + """Loads JSON from a file if it exists.""" + if os.path.exists(file_path): + try: + with open(file_path, "r") as f: + return json.load(f) + except Exception as e: + print(f"Warning: Failed to load {file_path}: {e}", file=sys.stderr) + return None + + +def aggregate_findings(temp_dir, taxonomy): + """Consolidates audit findings from workers. + + Produces a unified report data object. + """ + play_store_info = ( + load_json(os.path.join(temp_dir, "play_store_info.json")) or {} + ) + + aggregated_findings_path = os.path.join(temp_dir, "aggregated_findings.json") + + raw_findings = [] + critic_decisions = {} + + if os.path.exists(aggregated_findings_path): + master_data = load_json(aggregated_findings_path) or {} + raw_findings = master_data.get("findings", []) + + # Load all chunked critic outputs: critic_output_*.json + critic_files = sorted( + glob.glob(os.path.join(temp_dir, "critic_output_*.json")) + ) + for c_file in critic_files: + c_data = load_json(c_file) or {} + for fid, dec in c_data.items(): + critic_decisions[fid] = dec + + # 2. Process Findings and Apply Decoupled Critic Verdicts + identified_risks = [] + manual_review_needed = [] + data_safety_inventory = [] + + # Sort raw findings by finding_id numerically for stable processing order + def finding_sort_key(f): + fid = f.get("finding_id", "0") + try: + return int(fid) + except (ValueError, TypeError): + return 0 + + sorted_raw_findings = sorted(raw_findings, key=finding_sort_key) + + for finding in sorted_raw_findings: + p_id = finding.get("policy_id", "Unknown") + psl = finding.get("psl_constant") + fid = finding.get("finding_id") + wf = finding.get("worker_file") + + # Retrieve decision + decision = critic_decisions.get(str(fid)) + if not decision: + # Automatically approve/verify findings that bypassed the Critic + # (like SUGGESTIONs) + decision = { + "action": "VERIFIED", + "confidence": "High", + "critic_justification": ( + "Automatically verified (bypassed critic review)." + ), + } + + action = str(decision.get("action", "VERIFIED")).upper().strip() + confidence = decision.get("confidence", "Medium") + justification = decision.get("critic_justification", "Verified by Critic.") + + # Apply Critic's editorial overrides if specified + severity = decision.get("severity") or finding.get("severity", "SUGGESTION") + severity = clean_severity(severity) + + issue_summary = decision.get("issue_summary") or finding.get( + "issue_summary", "Unknown Issue" + ) + recommendation = decision.get("recommendation") or finding.get( + "recommendation", "" + ) + + files_involved = finding.get("files_involved", []) + if isinstance(files_involved, list): + files_involved = sorted([str(f) for f in files_involved]) + + evidence = finding.get("evidence", "") + + reconstructed_finding = { + "policy_id": p_id, + "finding_id": fid or f"{wf}_{p_id}", + "issue_summary": issue_summary, + "severity": severity, + "files_involved": files_involved, + "evidence": ( + f"{evidence}\n\n**Critic Verification**: {justification}" + f" (Confidence: {confidence})" + ), + "recommendation": recommendation, + } + + # Programmatically evaluate if this is a compliant Data Safety finding + # to avoid card clutter + is_compliant_ds = False + if "is_transferred" in finding: + is_transferred = parse_boolean(finding.get("is_transferred")) + user_initiated = parse_boolean(finding.get("user_initiated")) + is_third_party = parse_boolean(finding.get("is_third_party")) + disc_status = ( + str(finding.get("prominent_disclosure_status", "MISSING")) + .upper() + .strip() + ) + + # Compliant if either local-only OR transmitted but disclosed/exempt + # OR user initiated + if not is_transferred: + is_compliant_ds = True + elif user_initiated: + is_compliant_ds = True + elif "DISCLOSED" in disc_status or "EXEMPT" in disc_status: + is_compliant_ds = True + + # Route the finding based on Critic's action + if action == "VERIFIED": + if not is_compliant_ds: + identified_risks.append(reconstructed_finding) + elif action == "MANUAL_REVIEW": + if not is_compliant_ds: + manual_review_needed.append(reconstructed_finding) + + # 3. Process Data Safety Inventory Programmatically (If it's a DS Finding) + if "is_transferred" in finding: + # Create a localized copy of the data safety keys (psl_constant is + # natively preserved) + inv_item = { + "psl_constant": psl, + "is_transferred": parse_boolean(finding.get("is_transferred")), + "user_initiated": parse_boolean(finding.get("user_initiated")), + "is_third_party": parse_boolean(finding.get("is_third_party")), + "prominent_disclosure_status": ( + str(finding.get("prominent_disclosure_status", "MISSING")) + .upper() + .strip() + ), + "purpose": finding.get("purpose", "N/A"), + "linked_to_user": parse_boolean(finding.get("linked_to_user")), + "behavioral_proof": evidence, + "disclosure_proof": recommendation, + } + + # Programmatically align/vet inventory based on Critic's verdict + if action == "PRUNED": + inv_item["is_transferred"] = False + inv_item["purpose"] = "Local functionality only" + inv_item["behavioral_proof"] = f"Pruned by Critic: {justification}" + + data_safety_inventory.append(inv_item) + + # 4. Consolidate and Deduplicate Data Safety Inventory by psl_constant + merged_inventory = {} + for item in data_safety_inventory: + psl_id = item.get("psl_constant") + if not psl_id: + continue + + if psl_id not in merged_inventory: + merged_inventory[psl_id] = { + "psl_constant": psl_id, + "is_transferred": False, + "user_initiated": True, # Default to True for AND logic + "is_third_party": False, + "linked_to_user": False, + "prominent_disclosure_status": "EXEMPT", + "purposes": set(), + "behavioral_proofs": set(), + "disclosure_proofs": set(), + } + + current = merged_inventory[psl_id] + + # Transmission (True if any is True) + if item.get("is_transferred"): + current["is_transferred"] = True + + # User Initiated (True only if ALL are True) + if not item.get("user_initiated"): + current["user_initiated"] = False + + # Third Party (True if any is True) + if item.get("is_third_party"): + current["is_third_party"] = True + + # Linked (True if any is True) + if item.get("linked_to_user"): + current["linked_to_user"] = True + + # Disclosure logic precedence: MISSING > DISCLOSED > EXEMPT + new_disc = ( + str(item.get("prominent_disclosure_status", "EXEMPT")).upper().strip() + ) + curr_disc = current["prominent_disclosure_status"] + + if "MISSING" in curr_disc or "MISSING" in new_disc: + current["prominent_disclosure_status"] = "MISSING" + elif "DISCLOSED" in curr_disc or "DISCLOSED" in new_disc: + current["prominent_disclosure_status"] = "DISCLOSED" + else: + current["prominent_disclosure_status"] = "EXEMPT" + + if item.get("purpose") and item.get("purpose") != "N/A": + current["purposes"].add(str(item["purpose"]).strip()) + if item.get("behavioral_proof") and item.get("behavioral_proof") != "N/A": + current["behavioral_proofs"].add(str(item["behavioral_proof"]).strip()) + if item.get("disclosure_proof") and item.get("disclosure_proof") != "N/A": + current["disclosure_proofs"].add(str(item["disclosure_proof"]).strip()) + + # Reconstruct the list with joined sets + normalized_inventory = [] + sorted_psl_ids = sorted(merged_inventory.keys()) + for psl_id in sorted_psl_ids: + item = merged_inventory[psl_id] + reconstructed = { + "psl_constant": psl_id, + "is_transferred": "Yes" if item["is_transferred"] else "No", + "user_initiated": "Yes" if item.get("user_initiated") else "No", + "is_third_party": "Yes" if item.get("is_third_party") else "No", + "linked_to_user": "Yes" if item["linked_to_user"] else "No", + "prominent_disclosure_status": parse_disclosure_status( + item["prominent_disclosure_status"] + ), + "purpose": ", ".join(sorted(list(item["purposes"]))) or "N/A", + "behavioral_proof": ( + ", ".join(sorted(list(item["behavioral_proofs"]))) or "N/A" + ), + "disclosure_proof": ( + ", ".join(sorted(list(item["disclosure_proofs"]))) or "N/A" + ), + } + normalized_inventory.append(reconstructed) + + # 5. Decorate and Filter Data Safety Inventory + decorated_inventory = [] + local_access_only = [] + + for item in normalized_inventory: + psl_id = item.get("psl_constant") + tax_info = taxonomy.get(psl_id, {"category": "Other", "data_type": "Other"}) + item["category"] = tax_info["category"] + item["data_type"] = tax_info["data_type"] + + is_local = ( + item.get("is_transferred") == "No" + or item.get("purpose") == "Local functionality only" + ) + if is_local: + local_access_only.append(item) + else: + decorated_inventory.append(item) + + # Cross-reference with Play Store + matches = [] + mismatches = [] + is_published = play_store_info.get("is_published", False) + + if is_published: + play_declarations = play_store_info.get("data_safety", {}).get( + "data_collected", [] + ) + play_data_types = set() + for category_dict in play_declarations: + for type_dict in category_dict.get("types", []): + play_data_types.add(type_dict.get("type")) + + for item in decorated_inventory: + dt = item["data_type"] + if dt in play_data_types: + matches.append({"data_type": dt, "status": "Declared and detected"}) + else: + is_obvious = "Exempt" in item.get( + "prominent_disclosure_status", "" + ) or "Obvious" in item.get("prominent_disclosure_status", "") + mismatches.append({ + "data_type": dt, + "local_view": ( + f"Detected in code (Evidence: {item.get('behavioral_proof')})" + ), + "play_view": "Not declared in Play Store", + "status": ( + "Exempt: Obvious core functionality" + if is_obvious + else "Discrepancy" + ), + }) + + # Check for unjustified declarations (in Play Store but not in code) + detected_data_types = { + item["data_type"] for item in decorated_inventory + local_access_only + } + for category_dict in play_declarations: + for type_dict in category_dict.get("types", []): + dt = type_dict.get("type") + if dt not in detected_data_types: + mismatches.append({ + "data_type": dt, + "local_view": "Not detected in code", + "play_view": "Declared in Play Store", + "status": "Discrepancy", + }) + + # 7. Determine Compliance + overall_compliance = "Compliant" + critical_risks = any( + clean_severity(r.get("severity")) == "CRITICAL" for r in identified_risks + ) + active_mismatches = any(m.get("status") == "Discrepancy" for m in mismatches) + + if critical_risks or active_mismatches: + overall_compliance = "Non-compliant" + elif manual_review_needed or any( + clean_severity(r.get("severity")) == "IMPORTANT" for r in identified_risks + ): + overall_compliance = "Needs review" + + # 8. Sort result lists for deterministic output + def report_finding_sort_key(f): + pid = f.get("policy_id", "") + fid = f.get("finding_id", "0") + try: + num_fid = int(fid) + except (ValueError, TypeError): + num_fid = 0 + return (pid, num_fid) + + identified_risks.sort(key=report_finding_sort_key) + manual_review_needed.sort(key=lambda x: x.get("issue_summary", "")) + matches.sort(key=lambda x: x.get("data_type", "")) + mismatches.sort(key=lambda x: x.get("data_type", "")) + + # 9. Generate Summary + + risk_count = len(identified_risks) + mismatch_count = len( + [m for m in mismatches if m.get("status") == "Discrepancy"] + ) + exempt_count = len([ + m + for m in mismatches + if m.get("status") == "Exempt: Obvious core functionality" + ]) + + summary_parts = [] + summary_parts.append( + "The automated audit of" + f" {play_store_info.get('title', 'the application')} is complete." + ) + summary_parts.append( + f"Identified {risk_count} potential policy risks and {mismatch_count}" + " active Data Safety discrepancies." + ) + if exempt_count > 0: + summary_parts.append( + f"{exempt_count} detections were flagged as 'Obvious' and exempt from" + " prominent disclosure." + ) + + if overall_compliance == "Non-compliant": + summary_parts.append( + "Immediate remediation is required for critical findings and" + " declaration mismatches." + ) + elif overall_compliance == "Needs review": + summary_parts.append( + "Manual review is recommended for several ambiguous findings." + ) + else: + summary_parts.append( + "The application appears broadly compliant with analyzed policies." + ) + + # 9. Final Report Construction + report = { + "overall_compliance": overall_compliance, + "summary": " ".join(summary_parts), + "package_name": play_store_info.get("package_name", "unknown"), + "is_published": is_published, + "identified_risks": identified_risks, + "data_safety_comparison": {"matches": matches, "mismatches": mismatches}, + "local_data_access": [ + { + "data_type": item["data_type"], + "category": item["category"], + "evidence": item.get("behavioral_proof"), + } + for item in local_access_only + ], + "manual_review_needed": [ + {"issue_summary": r.get("issue_summary")} + for r in manual_review_needed + ], + "suggested_data_safety_declaration": { + "collected_data": [ + { + "data_type": item["data_type"], + "purpose": item.get("purpose"), + "linked_to_user": item.get("linked_to_user") == "Yes", + } + for item in decorated_inventory + ] + }, + } + + return report + + +def main(): + """Main entry point for report generation.""" + parser = argparse.ArgumentParser(description="Compliance report generator.") + parser.add_argument( + "temp_dir", + help="Path to the temporary scratch directory containing audit findings.", + ) + + args = parser.parse_args() + temp_dir = os.path.abspath(args.temp_dir) + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + policy_refs = ( + load_json(os.path.join(repo_root, "resources", "policies.json")) + ) + if not policy_refs: + policy_refs = {} + taxonomy = policy_refs.get("data_safety_section", {}).get("taxonomy", {}) + + manifest_details = ( + load_json(os.path.join(temp_dir, "manifest_details.json")) or {} + ) + package_name = manifest_details.get("package_name") + + # Load Play Store info (potentially ingested by orchestrator) + play_store_info_path = os.path.join(temp_dir, "play_store_info.json") + play_store_info = {} + if os.path.exists(play_store_info_path): + play_store_info = load_json(play_store_info_path) or {} + + if not play_store_info and package_name: + play_store_info = run_scraper(package_name, temp_dir) + # Cache it for report reuse + with open(play_store_info_path, "w") as f: + json.dump(play_store_info, f, indent=4, sort_keys=True) + + report_data = aggregate_findings(temp_dir, taxonomy) + + template_path = os.path.join( + repo_root, "resources", "compliance_report_template.md" + ) + if not os.path.exists(template_path): + print(f"Error: Template not found at {template_path}", file=sys.stderr) + sys.exit(1) + + try: + with open(template_path, "r") as f: + content = f.read() + except Exception as e: + print(f"Error reading template: {e}", file=sys.stderr) + sys.exit(1) + + output_path = os.path.join(temp_dir, "compliance_report.md") + + violations = report_data.get("identified_risks", []) + mismatches = report_data.get("data_safety_comparison", {}).get( + "mismatches", [] + ) + manual = report_data.get("manual_review_needed", []) + + compliance_raw = report_data.get("overall_compliance") or "Needs review" + status_map = { + "Non-compliant": "🔴 Non-Compliant", + "Needs review": "🟡 Needs Review", + "Compliant": "🟢 Compliant", + } + compliance_ui = status_map.get(compliance_raw, "🟡 Needs Review") + + app_id = report_data.get("package_name") or "Unknown App" + + findings_content = "" + if not violations: + findings_content = "\n* No policy risks identified in this scan.\n" + else: + grouped_findings = {} + for v in violations: + p_id = v.get("policy_id") + macro_cat = "Other Policies" + if p_id and p_id in policy_refs: + macro_cat = policy_refs[p_id].get("category", "Other Policies") + + if macro_cat not in grouped_findings: + grouped_findings[macro_cat] = [] + grouped_findings[macro_cat].append(v) + + ordered_cats = [ + "Restricted Content", + "Privacy, Deception and Device Abuse", + "Monetization and Ads", + "Store Listing and Promotion", + "Developer Account Management", + ] + + all_cats = ordered_cats + [ + cat for cat in grouped_findings if cat not in ordered_cats + ] + + for cat in all_cats: + if cat in grouped_findings: + findings_content += f"\n### {cat}\n" + for v in grouped_findings[cat]: + findings_content += render_finding_card(v, policy_refs) + "\n" + + is_not_published = report_data.get("is_published") is False + + d_table_msg = "Code detection matches Play Store declarations." + if is_not_published: + d_table_msg = ( + "N/A - App is not yet published. No declarations found for comparison." + ) + + d_table = render_table( + mismatches, + ["data_type", "local_view", "play_view", "status"], + ["Data Type", "Code Detection", "Play Store Declaration", "Status"], + d_table_msg, + ) + + # Local Access Section + local_access = report_data.get("local_data_access", []) + la_content = "" + if local_access: + la_table = render_table( + local_access, + ["data_type", "category", "evidence"], + ["Data Type", "Category", "Access Evidence"], + "No local-only access detected.", + ) + la_content = ( + "## Local Data Access (No Transmission)\nThe following data types are" + " accessed by the code but no evidence of network transmission or" + " exfiltration was detected. These typically do not require a" + f" 'Collection' declaration in the Data Safety section.\n{la_table}" + ) + + # Suggested Declaration (Conditional Section) + suggested_dec = report_data.get("suggested_data_safety_declaration", {}) + dec_content = "" + if suggested_dec and any(suggested_dec.values()): + dec_content = ( + "### Suggested Data Safety Declaration" + f" Updates\n```json\n{json.dumps(suggested_dec, indent=4)}\n```" + ) + + checklist_items = [] + for m in manual: + m_issue = strip_emojis( + m.get("issue") or m.get("issue_summary") or "Review item" + ) + checklist_items.append(f"- [ ] {m_issue}") + + risk_categories = set( + [v.get("category") for v in violations if v.get("category")] + ) + mapping = { + "Undeclared Collection": "Update Data Safety section in Play Console.", + "Permissions": "Review and minimize requested permissions.", + } + for cat in risk_categories: + if cat in mapping: + checklist_items.append(f"- [ ] {mapping[cat]}") + if not checklist_items: + checklist_items.append("- [ ] Review all identified policy risks.") + + template_context = { + "overall_compliance": compliance_ui, + "current_date": datetime.now().strftime("%Y-%m-%d"), + "app_name_id": app_id, + "findings_detail": findings_content, + "data_safety_table": d_table, + "local_access_section": la_content, + "suggested_declaration_section": dec_content, + "personalized_checklist": "\n".join(checklist_items), + } + + content = render_template(content, template_context) + + with open(output_path, "w") as f: + f.write(content) + + json_output_path = output_path.replace(".md", ".json") + with open(json_output_path, "w") as f: + json.dump( + { + "metadata": { + "app_id": app_id, + "scan_date": datetime.now().isoformat(), + "overall_compliance": compliance_raw, + }, + "findings": violations, + "data_safety_mismatches": mismatches, + "manual_review": manual, + "suggested_declaration": suggested_dec, + }, + f, + indent=4, + sort_keys=True, + ) + print(f"Reports generated successfully at {output_path}") + + +if __name__ == "__main__": + main() diff --git a/play/play-policy-insights/scripts/orchestrator.py b/play/play-policy-insights/scripts/orchestrator.py new file mode 100755 index 0000000..6cb5b20 --- /dev/null +++ b/play/play-policy-insights/scripts/orchestrator.py @@ -0,0 +1,1207 @@ +#!/usr/bin/env python3 + +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Coordinates the end-to-end Play Policy audit flow. + +This manages everything from triage to analysis aggregation. +""" + +import argparse +import concurrent.futures +import glob +import json +import os +import re +import sys +import uuid +import xml.etree.ElementTree as ET + +import scanner +from template_engine import render_template + +# Constants for Gradle parsing +GRADLE_APP_ID_PATTERN = ( + r'applicationId\s*(?:[:=]|\.set\(?)\s*[\'"]?([a-zA-Z0-9._]+)[\'"]?\)?' +) +GRADLE_APP_ID_PROP_PATTERN = ( + r'applicationId\s*[:=]?\s*project\.property\(\s*[\'"]([^\'"]+)[\'"]\s*\)' +) +GRADLE_NAMESPACE_PATTERN = ( + r'namespace\s*(?:[:=]|\.set\(?)\s*[\'"]?([a-zA-Z0-9._]+)[\'"]?\)?' +) +GRADLE_NAMESPACE_PROP_PATTERN = ( + r'namespace\s*[:=]?\s*project\.property\(\s*[\'"]([^\'"]+)[\'"]\s*\)' +) +GRADLE_TARGET_SDK_PATTERN = ( + r"targetSdk(?:Version)?(?:\.set\(?)?\s*[:=" + r" (]*\s*([\'\"]?[\w\d_\.]+[\'\"]?)\)?" +) + +# Well-known framework defaults for unresolved dynamic variables (e.g. Flutter) +FRAMEWORK_DEFAULTS = { + "flutter.targetSdkVersion": 35, + "flutter.compileSdkVersion": 35, + "flutter.minSdkVersion": 21, + "flutter.ndkVersion": "25.1.8937393", +} + +# Patterns for diverse assignment styles (Groovy & Kotlin DSL) +# 1. Standard: targetSdkVersion = 35, ext.targetSdkVersion = 35, etc. +# 2. Delegated: val targetSdkVersion by extra(35) +# 3. Functional: set("targetSdkVersion", 35) or +# extra.set("targetSdkVersion", 35) +# 4. Indexer: extra["targetSdkVersion"] = 35 +GRADLE_ASSIGNMENT_PATTERNS = [ + re.compile( + r"(?:\b(?:val|var|def|const|static|internal|private|" + r"public|protected|ext|final)\s+)*" + r"([\w\d_\.]+)(?:\s*:\s*[\w\d_\.]+)?\s*=\s*['\"]?" + r"([^\x27\"\s\(\)<>\{\}]+)['\"]?" + ), + re.compile( + r"val\s+([\w\d_\.]+)(?:\s*:\s*[\w\d_\.]+)?\s+by\s+" + r"extra\s*\(\s*['\"]?([^\x27\"\s\(\)]+)['\"]?\s*\)" + ), + re.compile( + r"(?:extra\.)?set\s*\(\s*['\"]([\w\d_\.]+)['\"]\s*,\s*" + r"['\"]?([^\x27\"\s\(\)]+)['\"]?\s*\)" + ), + re.compile( + r"extra\s*\[\s*['\"]([\w\d_\.]+)['\"]\s*\]\s*=\s*" + r"['\"]?([^\x27\"\s]+)['\"]?" + ), +] + + +def _strip_inline_comment(line, support_quotes=True): + """Strips inline comments starting with '#' while preserving quotes.""" + in_double = False + in_single = False + for i, char in enumerate(line): + if support_quotes: + if char == '"' and not in_single: + in_double = not in_double + elif char == "'" and not in_double: + in_single = not in_single + + if char == "#" and not in_double and not in_single: + return line[:i] + return line + + +def load_gradle_properties(target_dir): + """Loads properties from gradle.properties and local.properties.""" + properties = {} + for filename in ["gradle.properties", "local.properties"]: + path = os.path.join(target_dir, filename) + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = _strip_inline_comment(line, support_quotes=False).strip() + if line and not line.startswith("#") and "=" in line: + key, val = line.split("=", 1) + properties[key.strip()] = val.strip() + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to load properties from {path}: {e}", + file=sys.stderr, + ) + return properties + + +def load_version_catalog(target_dir): + """Loads versions from gradle/libs.versions.toml if it exists.""" + catalog = {} + toml_path = os.path.join(target_dir, "gradle", "libs.versions.toml") + if os.path.exists(toml_path): + try: + with open(toml_path, "r", encoding="utf-8", errors="ignore") as f: + current_section = None + for line in f: + line = _strip_inline_comment(line).strip() + if not line or line.startswith("#"): + continue + if line.startswith("[") and line.endswith("]"): + current_section = line[1:-1].strip() + continue + if current_section == "versions" and "=" in line: + key, val = line.split("=", 1) + val = val.strip().strip('"').strip("'") + catalog[key.strip()] = val.strip() + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to load version catalog from {toml_path}: {e}", + file=sys.stderr, + ) + return catalog + + +def load_custom_gradle_versions(target_dir, file_inventory=None): + """Loads versions from shared Gradle files and project configuration.""" + versions = {} + if not target_dir or not os.path.isdir(target_dir): + return versions + + # 1. Identify Candidate Files (Root files are the primary targets) + candidate_files = [] + + # Leaking Strategy: If target_dir is a sub-module, leak up to find the root + # e.g. /path/to/project/app -> check /path/to/project for root build files + search_roots = [target_dir] + parent = os.path.dirname(os.path.normpath(target_dir)) + if parent and parent != target_dir: + search_roots.append(parent) + + for s_root in search_roots: + for f in ["build.gradle", "build.gradle.kts"]: + root_file = os.path.normpath(os.path.join(s_root, f)) + if os.path.exists(root_file) and root_file not in candidate_files: + candidate_files.append(root_file) + + # Use inventory if provided to avoid redundant file walk + if file_inventory: + search_lists = [ + file_inventory.get("gradles", []), + file_inventory.get("source_code", []), + ] + for file_list in search_lists: + for path in file_list: + norm_path = os.path.normpath(path) + if norm_path not in candidate_files: + file_lower = os.path.basename(norm_path).lower() + if any( + name in file_lower + for name in [ + "versions.gradle", + "dependencies.gradle", + "variables.gradle", + ] + ): + candidate_files.append(norm_path) + elif any( + d in norm_path.lower() for d in ["build-logic", "buildsrc"] + ) and file_lower.endswith((".gradle", ".gradle.kts", ".kt")): + candidate_files.append(norm_path) + else: + # Fallback to walk if no inventory provided + config = scanner.get_scanner_config() + ignored_dirs = set(config.get("ignored_directories", [])) + for root, dirs, files in os.walk(target_dir): + dirs[:] = [d for d in dirs if d not in ignored_dirs] + for f in files: + if f.lower().endswith((".gradle", ".gradle.kts", ".kt")): + candidate_files.append(os.path.join(root, f)) + + # 2. Parse candidate files for assignments + for f_path in candidate_files: + try: + with open(f_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + for pattern in GRADLE_ASSIGNMENT_PATTERNS: + for key, val in pattern.findall(content): + # Clean up the key (remove 'ext.' or 'project.' prefixes) + clean_key = key.split(".")[-1] + if clean_key not in versions: + versions[clean_key] = val.strip().strip('"').strip("'") + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to parse Gradle file {f_path}: {e}", file=sys.stderr + ) + + return versions + + +def _load_json_file(path): + """Safely loads a JSON file.""" + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to load JSON from {path}: {e}", file=sys.stderr) + return {} + + +def _read_text_file(path): + """Safely reads a text file.""" + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to read file {path}: {e}", file=sys.stderr) + return "" + + +def _write_json_file(path, data): + """Safely writes a JSON file.""" + try: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4, sort_keys=True) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to write JSON to {path}: {e}", file=sys.stderr) + + +def _write_text_file(path, content): + """Safely writes a text file.""" + try: + with open(path, "w", encoding="utf-8") as f: + f.write(content) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to write file {path}: {e}", file=sys.stderr) + + +def _filter_by_flavor(data, detected_flavors, prioritized_flavors): + """Recursively filters lists and dicts based on flavor path segments.""" + if isinstance(data, dict): + filtered_dict = {} + for k, v in data.items(): + filtered_val = _filter_by_flavor(v, detected_flavors, prioritized_flavors) + if isinstance(filtered_val, (dict, list)) and not filtered_val: + continue + filtered_dict[k] = filtered_val + return filtered_dict + + elif isinstance(data, list): + filtered_list = [] + for entry in data: + # Handle tuple entries (like in codebase_map.all_files) + entry_str = ( + entry[1] + if isinstance(entry, tuple) and len(entry) > 1 + else str(entry) + ) + + is_excluded = any( + f not in prioritized_flavors and f"src/{f}/" in entry_str + for f in detected_flavors + ) + if not is_excluded: + filtered_list.append(entry) + return filtered_list + + return data # Fallback for primitive types + + +def parse_application_modules( + gradle_files, + manifest_files, + gradle_properties, + version_catalog, + custom_gradle_versions=None, +): + """Identifies distinct application modules in the project.""" + modules = [] + lookups = { + "properties": gradle_properties, + "catalog": version_catalog, + "custom": custom_gradle_versions or {}, + } + + # First, check Gradle files for the application plugin + for g_file in gradle_files: + try: + with open(g_file, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + if re.search(GRADLE_APP_PLUGIN_PATTERN, content): + # Found an app module! + mod_dir = os.path.dirname(g_file) + mod_name = os.path.basename(mod_dir) + + # Try to extract details from this Gradle file + app_id = None + target_sdk = None + app_label = None + + id_match = re.search(GRADLE_APP_ID_PATTERN, content) + if id_match: + app_id = id_match.group(1) + else: + prop_match = re.search(GRADLE_APP_ID_PROP_PATTERN, content) + if prop_match: + app_id = lookups["properties"].get(prop_match.group(1)) + + sdk_match = re.search(GRADLE_TARGET_SDK_PATTERN, content) + if sdk_match: + target_sdk = _resolve_to_int(sdk_match.group(1), lookups) + + # Fallback to namespace if applicationId is missing + if not app_id: + ns_match = re.search(GRADLE_NAMESPACE_PATTERN, content) + if ns_match: + app_id = ns_match.group(1) + else: + ns_prop_match = re.search(GRADLE_NAMESPACE_PROP_PATTERN, content) + if ns_prop_match: + app_id = lookups["properties"].get(ns_prop_match.group(1)) + + modules.append({ + "name": mod_name, + "path": mod_dir, + "application_id": app_id, + "target_sdk": target_sdk, + "app_label": app_label, + }) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to parse Gradle file {g_file}: {e}", file=sys.stderr + ) + + return modules + + +def determine_primary_identity( + app_modules, + gradle_files, + manifest_files, + gradle_properties, + version_catalog, + custom_gradle_versions, + app_dir, +): + """Heuristically determines the primary app ID and target SDK.""" + primary_id = None + primary_sdk = None + primary_label = None + + # 1. Prefer explicitly defined app modules + for mod in app_modules: + if mod.get("application_id"): + primary_id = mod["application_id"] + primary_sdk = mod["target_sdk"] + primary_label = mod["app_label"] + break + + # 2. Fallback to global Gradle properties + if not primary_id: + primary_id = gradle_properties.get("applicationId") or ( + gradle_properties.get("namespace") + ) + if not primary_sdk: + primary_sdk = _resolve_to_int( + gradle_properties.get("targetSdkVersion") + or gradle_properties.get("targetSdk"), + {"properties": gradle_properties, "catalog": version_catalog}, + ) + + # 3. Fallback to scanning manifests if still missing + if not primary_id or not primary_sdk: + for m_file in manifest_files: + try: + with open(m_file, "r", encoding="utf-8") as f: + content = f.read() + details = extract_manifest_details(content) + if not primary_id: + primary_id = details.get("package_name") + if not primary_sdk: + primary_sdk = details.get("target_sdk") + if not primary_label: + primary_label = details.get("app_label") + except Exception: # pylint: disable=broad-exception-caught + pass + + # 4. Cross-Platform Framework Detection + if not primary_id or not primary_sdk: + # Check for Flutter (pubspec.yaml) + pubspec_path = os.path.join(app_dir, "pubspec.yaml") + if os.path.exists(pubspec_path): + try: + with open(pubspec_path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("name:"): + primary_id = line.split(":")[1].strip() + break + primary_sdk = FRAMEWORK_DEFAULTS.get("flutter.targetSdkVersion") + except Exception: # pylint: disable=broad-exception-caught + pass + + # Check for React Native (package.json) + pkg_json_path = os.path.join(app_dir, "package.json") + if os.path.exists(pkg_json_path): + try: + with open(pkg_json_path, "r", encoding="utf-8") as f: + data = json.load(f) + primary_id = data.get("name") + except Exception: # pylint: disable=broad-exception-caught + pass + + return primary_id, primary_sdk, primary_label + + +def extract_manifest_details(xml_content): + """Parses AndroidManifest.xml for key policy signals.""" + details = { + "package_name": None, + "target_sdk": None, + "app_label": None, + "permissions": [], + "foreground_services": [], + } + + try: + # Handle XML with namespaces + root = ET.fromstring(xml_content) + ns = {"android": "http://schemas.android.com/apk/res/android"} + + details["package_name"] = root.get("package") + + # Target SDK + uses_sdk = root.find("uses-sdk", ns) + if uses_sdk is not None: + details["target_sdk"] = uses_sdk.get( + "{http://schemas.android.com/apk/res/android}targetSdkVersion" + ) + + # App Label + application = root.find("application", ns) + if application is not None: + label = application.get( + "{http://schemas.android.com/apk/res/android}label" + ) + if label and label.startswith("@string/"): + details["app_label"] = label[8:] + else: + details["app_label"] = label + + # Permissions + for tag in ["uses-permission", "uses-permission-sdk-23"]: + for perm in root.findall(tag, ns): + name = perm.get("{http://schemas.android.com/apk/res/android}name") + if name: + details["permissions"].append(name) + + # Permissions from component declarations + for tag in [ + "service", + "receiver", + "activity", + "provider", + "activity-alias", + ]: + for component in root.findall(f".//{tag}", ns): + perm = component.get( + "{http://schemas.android.com/apk/res/android}permission" + ) + if perm: + details["permissions"].append(perm) + + if tag == "provider": + read_perm = component.get( + "{http://schemas.android.com/apk/res/android}readPermission" + ) + if read_perm: + details["permissions"].append(read_perm) + write_perm = component.get( + "{http://schemas.android.com/apk/res/android}writePermission" + ) + if write_perm: + details["permissions"].append(write_perm) + + # Foreground Services + for service in root.findall(".//service", ns): + # Check for foregroundServiceType + fgs_type = service.get( + "{http://schemas.android.com/apk/res/android}foregroundServiceType" + ) + if fgs_type: + details["foreground_services"].append({ + "name": service.get( + "{http://schemas.android.com/apk/res/android}name" + ), + "type": fgs_type, + }) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to parse XML: {e}", file=sys.stderr) + + return details + + +def generate_triage_summary(manifest_details, data_safety_scan): + """Generates a high-level summary of the audit landscape.""" + summary = [] + + package = manifest_details.get("package_name") or "Unknown" + summary.append(f"Audit Target: {package}") + + sdk = manifest_details.get("target_sdk") or "Unknown" + summary.append(f"Target SDK: {sdk}") + + perms_count = len(manifest_details.get("permissions", [])) + summary.append(f"Permissions Requested: {perms_count}") + + signals_count = 0 + if isinstance(data_safety_scan, dict): + inner_scan = data_safety_scan.get("data_safety_scan", {}) + if "data_sources" in inner_scan: + signals_count = len(inner_scan["data_sources"]) + summary.append(f"Sensitive Data Signals Found: {signals_count}") + + return "\n".join(summary) + + +def run_scanner_direct(target_dir, file_inventory, output_file): + """Invokes the scanner directly using the provided inventory.""" + try: + # This calls the internal scan logic from scanner.py + results = scanner.perform_scan(target_dir, file_inventory) + _write_json_file(output_file, results) + return results + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error during scan: {e}", file=sys.stderr) + return {} + + +def write_agent_prompts(env_data, goal_map, output_data): + """Generates surgical prompt files for worker agents and infers activation.""" + repo_root = env_data.get("repo_root") + temp_dir = env_data.get("temp_dir") + app_dir = env_data.get("app_dir") + activated_goals = [] + + # 1. Load Shared Resources + policies_path = os.path.join(repo_root, "resources", "policies.json") + policies_data = _load_json_file(policies_path) + taxonomy = policies_data.get("data_safety_section", {}).get("taxonomy", {}) + taxonomy_keys = set(taxonomy.keys()) + + common_mandates_path = os.path.join( + repo_root, "resources", "common_mandates.md" + ) + common_mandates = _read_text_file(common_mandates_path) + + config = scanner.get_scanner_config() + permission_groups = config.get("permission_groups", {}) + + # 2. Write Slices + if output_data: + for slice_name in ["manifest_details", "codebase_map"]: + slice_path = os.path.join(temp_dir, f"{slice_name}.json") + _write_json_file(slice_path, output_data.get(slice_name, {})) + + # 3. Build Base Context + scan_results = output_data.get("data_safety_scan", {}) + base_context = scan_results.get("data_safety_scan", {}).copy() + + # Also ensure other metadata is included + base_context["found_urls"] = scan_results.get("found_urls", []) + base_context["semantic_files"] = scan_results.get("semantic_files", {}) + + # Flatten anonymous categories (those with only a "-" key) + for cat in [ + "exact_alarm", + "accessibility", + "foreground_service", + "disclosure", + ]: + if ( + cat in base_context + and isinstance(base_context[cat], dict) + and "-" in base_context[cat] + ): + base_context[cat] = base_context[cat]["-"] + + manifest_details = output_data.get("manifest_details", {}) + base_context["TARGET_SDK"] = manifest_details.get("target_sdk") + base_context["APP_NAME"] = output_data.get("app_name") + base_context["PACKAGE_NAME"] = output_data.get("package_name") + manifest_perms = manifest_details.get("permissions", []) + + # Populate requested_permissions dictionary based on groups + requested_perms_dict = {} + for group_name, perms_to_check in permission_groups.items(): + matched = [p for p in perms_to_check if p in manifest_perms] + if matched: + requested_perms_dict[group_name] = matched + + base_context["requested_permissions"] = requested_perms_dict + base_context["TEMP_DIR"] = temp_dir + base_context["APP_DIR"] = app_dir + base_context["REPO_ROOT"] = repo_root + + # 4. Smart Filtering for data_sources (Noise Reduction) + if "data_sources" in base_context: + filtered_sources = {} + for data_type, findings in base_context["data_sources"].items(): + # Deduplicate by File (max 2 per unique file) + file_counts = {} + diverse_findings = [] + for f in findings: + file_path = f.split(":L")[0] if ":L" in f else f + if file_counts.get(file_path, 0) < 2: + diverse_findings.append(f) + file_counts[file_path] = file_counts.get(file_path, 0) + 1 + + # Global Cap (max 3 per data type) + filtered_sources[data_type] = diverse_findings[:3] + base_context["data_sources"] = filtered_sources + + max_evidence_per_worker = 6 + + # Helper to evaluate and save a goal instance + def _save_goal_instance(instance_name, context, template_content): + full_template = template_content + "\n\n" + common_mandates + final_prompt = render_template(full_template, context) + + if "{{ACTIVATE_GOAL}}" in final_prompt: + final_prompt = final_prompt.replace("{{ACTIVATE_GOAL}}", "") + + goal_input_file = os.path.join( + temp_dir, f"input_worker_{instance_name}.json" + ) + _write_json_file(goal_input_file, context) + + output_path = os.path.join(temp_dir, f"prompt_worker_{instance_name}.md") + _write_text_file(output_path, final_prompt) + return True + return False + + # 5. Process Goals Sequentially (Removes Concurrency GIL Overhead & State + # Copies) + for goal_name, goal_conf in goal_map.items(): + template_path = os.path.join( + repo_root, "resources", goal_conf["prompt_file"] + ) + template_content = _read_text_file(template_path) + if not template_content: + continue + + # Identify chunks for this goal + chunks = [] + + if goal_name == "data_safety" and "data_sources" in base_context: + all_evidence = [] + for data_type, findings in base_context["data_sources"].items(): + if not taxonomy_keys or data_type in taxonomy_keys: + for finding in findings: + all_evidence.append((data_type, finding)) + + for i in range(0, len(all_evidence), max_evidence_per_worker): + chunk = all_evidence[i : i + max_evidence_per_worker] + chunk_dict = {} + for dt, finding in chunk: + if dt not in chunk_dict: + chunk_dict[dt] = { + "description": taxonomy.get(dt, {}).get( + "description", "User data type." + ), + "findings": [], + } + chunk_dict[dt]["findings"].append(finding) + chunks.append(chunk_dict) + + if not chunks: + # Single pass for small goals + if _save_goal_instance(goal_name, base_context, template_content): + activated_goals.append(goal_name) + else: + # Multipass for large data safety sets + for idx, chunk_data in enumerate(chunks, 1): + instance_name = f"{goal_name}_part_{idx}" + chunk_context = base_context.copy() + chunk_context["data_sources"] = chunk_data + chunk_context["GOAL_NAME"] = instance_name + if _save_goal_instance(instance_name, chunk_context, template_content): + activated_goals.append(instance_name) + + return activated_goals + + +def run_aggregation(temp_dir, repo_root): + """Aggregates and chunks findings for the Critic.""" + worker_pattern = os.path.join(temp_dir, "worker_*.json") + worker_files = sorted(glob.glob(worker_pattern)) + + all_findings = [] + finding_id_counter = 1 + + for w_file in worker_files: + basename = os.path.basename(w_file) + try: + with open(w_file, "r") as f: + w_data = json.load(f) + findings_list = w_data.get("findings", []) + if isinstance(findings_list, list): + for finding in findings_list: + # Store the source filename so generate_report can associate it + # correctly + finding["worker_file"] = basename + # Assign sequential ID + finding["finding_id"] = str(finding_id_counter) + finding_id_counter += 1 + all_findings.append(finding) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to load worker file {w_file}: {e}", file=sys.stderr + ) + + # Save the master list of all findings + aggregated_path = os.path.join(temp_dir, "aggregated_findings.json") + with open(aggregated_path, "w") as f: + json.dump({"findings": all_findings}, f, indent=4) + + # Filter findings that need Critic evaluation: skip only SUGGESTION findings + critic_findings = [] + for f in all_findings: + severity = str(f.get("severity", "SUGGESTION")).upper().strip() + if severity == "SUGGESTION": + continue + critic_findings.append(f) + + # Chunk the findings for the Critic + chunk_size = 3 + chunks = [ + critic_findings[i : i + chunk_size] + for i in range(0, len(critic_findings), chunk_size) + ] + + # Render Critic template for each chunk + critic_template_path = os.path.join(repo_root, "resources", "critic.md") + critic_template = "" + if os.path.exists(critic_template_path): + with open(critic_template_path, "r") as f: + critic_template = f.read() + + common_mandates_path = os.path.join( + repo_root, "resources", "common_mandates.md" + ) + common_mandates = "" + if os.path.exists(common_mandates_path): + with open(common_mandates_path, "r") as f: + common_mandates = f.read() + + full_template = critic_template + "\n\n" + common_mandates + + # Load contextual values from manifest_details.json + manifest_path = os.path.join(temp_dir, "manifest_details.json") + base_context = {} + if os.path.exists(manifest_path): + try: + with open(manifest_path, "r") as f: + m_details = json.load(f) + base_context["APP_DIR"] = m_details.get("app_dir") + except Exception: # pylint: disable=broad-exception-caught + pass + + if not repo_root: + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + base_context["TEMP_DIR"] = temp_dir + base_context["REPO_ROOT"] = repo_root + + for idx, chunk in enumerate(chunks, 1): + # Create input_critic_.json + chunk_dict = {f["finding_id"]: f for f in chunk} + chunk_input_path = os.path.join(temp_dir, f"input_critic_{idx}.json") + with open(chunk_input_path, "w") as f: + json.dump(chunk_dict, f, indent=4) + + # Render prompt_critic_.md + chunk_context = base_context.copy() + chunk_context["CHUNK_INDEX"] = str(idx) + + final_prompt = render_template(full_template, chunk_context) + chunk_prompt_path = os.path.join(temp_dir, f"prompt_critic_{idx}.md") + with open(chunk_prompt_path, "w") as f: + f.write(final_prompt) + + return len(chunks) + + +GRADLE_APP_PLUGIN_PATTERN = ( + r"(?:id\s*\(?\s*[\x27\x22]com\.android\.application[\x27\x22]\s*\)?|" + r"apply\s*\(?\s*(?:plugin:\s*)?[\x27\x22]com\.android\.application" + r"[\x27\x22]\s*\)?)(?!\s+apply\s+false)" +) + + +def _resolve_to_int(val, lookups, visited=None): + """Recursively resolves a variable name to an integer value.""" + if visited is None: + visited = set() + + if not val: + return None + + # Clean and normalize the value + val_str = str(val).strip().strip('"').strip("'") + + # Support project.property("key") syntax + prop_match = re.search( + r'project\.property\s*\(\s*[\'"](.*?)[\'"]\s*\)', val_str + ) + if prop_match: + val_str = prop_match.group(1).strip() + + # Robust resolution for Gradle Version Catalogs and dynamic getters + # Strip common accessors and indirection prefixes + val_str = re.sub( + r"\.(?:get|toInt|getOrElse|provider)(?:\s*\([^)]*\))?", "", val_str + ) + if val_str.startswith("libs.versions."): + val_str = val_str[14:] + elif val_str.startswith("libs."): + val_str = val_str[5:] + elif val_str.startswith("versions."): + val_str = val_str[9:] + + if val_str.isdigit(): + return int(val_str) + + if val_str in visited: + return None # Circular reference protection + visited.add(val_str) + + # 1. Direct and Namespaced Lookups + search_keys = [val_str] + if "." in val_str: + # If A.B.C fails, try B.C, then C + parts = val_str.split(".") + for i in range(1, len(parts)): + search_keys.append(".".join(parts[i:])) + + for key in search_keys: + # Try custom versions first (usually has highest signal) + if key in lookups.get("custom", {}): + res = _resolve_to_int(lookups["custom"][key], lookups, visited) + if res is not None: + return res + + # Try gradle properties + if key in lookups.get("properties", {}): + res = _resolve_to_int(lookups["properties"][key], lookups, visited) + if res is not None: + return res + + # Try version catalog + if key in lookups.get("catalog", {}): + res = _resolve_to_int(lookups["catalog"][key], lookups, visited) + if res is not None: + return res + + return None + + +def main(): + """Deterministic context-gathering tool for the Reviewer skill.""" + parser = argparse.ArgumentParser( + description="Deterministic context-gathering and state-query tool." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + # Init subcommand + init_parser = subparsers.add_parser( + "init", help="Initialize scratch environment." + ) + init_parser.add_argument("app_dir", help="Path to the Android application.") + + # Aggregate subcommand + agg_parser = subparsers.add_parser("aggregate", help="Aggregate findings.") + agg_parser.add_argument( + "temp_dir", help="Path to the temporary scratch directory." + ) + + args = parser.parse_args() + + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + if args.command == "aggregate": + temp_dir = os.path.abspath(args.temp_dir) + if not os.path.isdir(temp_dir): + print( + f"Error: Specified temporary directory does not exist: {temp_dir}", + file=sys.stderr, + ) + sys.exit(1) + + chunk_count = run_aggregation(temp_dir, repo_root) + print(json.dumps({"temp_dir": temp_dir, "critic_chunks": chunk_count})) + sys.exit(0) + + # Initialization Mode + app_dir = os.path.abspath(args.app_dir) + if not os.path.isdir(app_dir): + print(f"Error: {app_dir} is not a valid directory.", file=sys.stderr) + sys.exit(1) + + scratch_id = str(uuid.uuid4()) + workspace_root = os.getcwd() + temp_dir = os.path.join( + workspace_root, ".scratch", f"play_policy_insights_{scratch_id}" + ) + os.makedirs(temp_dir, exist_ok=True) + + # Perform a single master filesystem walk using the scanner + file_inventory = scanner.collect_target_files(app_dir) + + gradle_properties = load_gradle_properties(app_dir) + version_catalog = load_version_catalog(app_dir) + # Pass inventory to avoid redundant walk + custom_gradle_versions = load_custom_gradle_versions(app_dir, file_inventory) + + codebase_map = { + "key_files": { + "manifests": file_inventory["manifests"], + "gradles": [ + g + for g in file_inventory["gradles"] + if g.endswith((".gradle", ".gradle.kts")) + ], + } + } + + # 1. Detect and Filter Flavors (Play Store Prioritization) + detected_flavors = set() + src_dirs = [] + + config = scanner.get_scanner_config() + ignored_dirs = set(config.get("ignored_directories", [])) + + # Find all 'src' directories + for root, dirs, _ in os.walk(app_dir): + dirs[:] = [d for d in dirs if d not in ignored_dirs] + if "src" in dirs: + src_dirs.append(os.path.join(root, "src")) + dirs.remove("src") # Don't recurse deeper for 'src' search + + for s_dir in src_dirs: + for flavor in os.listdir(s_dir): + if ( + os.path.isdir(os.path.join(s_dir, flavor)) + and flavor not in ignored_dirs + ): + detected_flavors.add(flavor) + + prioritized_flavors = [] + if "play" in detected_flavors: + prioritized_flavors = ["main", "play"] + + if prioritized_flavors: + # Filter codebase_map to only include prioritized flavors + print( + f"Prioritizing flavors: {', '.join(prioritized_flavors)}", + file=sys.stderr, + ) + + filtered_files = {} + for key, paths in codebase_map.get("key_files", {}).items(): + filtered_paths = [] + for p in paths: + # Check if path contains any non-prioritized flavor + is_excluded = False + for f in detected_flavors: + if f not in prioritized_flavors and f"src/{f}/" in p: + is_excluded = True + break + if not is_excluded: + filtered_paths.append(p) + filtered_files[key] = filtered_paths + codebase_map["key_files"] = filtered_files + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + # 1. Start the Data Safety Scanner immediately (independent task) + data_safety_scan_file = os.path.join(temp_dir, "data_safety_scan.json") + scanner_future = executor.submit( + run_scanner_direct, app_dir, file_inventory, data_safety_scan_file + ) + + # 2. Extract Application Modules and Primary Identity + app_modules = parse_application_modules( + codebase_map.get("key_files", {}).get("gradles", []), + codebase_map.get("key_files", {}).get("manifests", []), + gradle_properties, + version_catalog, + custom_gradle_versions=custom_gradle_versions, + ) + primary_app_id, primary_target_sdk, primary_app_label = ( + determine_primary_identity( + app_modules, + codebase_map.get("key_files", {}).get("gradles", []), + codebase_map.get("key_files", {}).get("manifests", []), + gradle_properties, + version_catalog, + custom_gradle_versions, + app_dir, + ) + ) + + manifest_content = "" + manifest_details = { + "package_name": primary_app_id, + "app_label": primary_app_label, + "target_sdk": primary_target_sdk, + "app_dir": app_dir, + "permissions": [], + "foreground_services": [], + } + + if codebase_map.get("key_files", {}).get("manifests"): + for _, manifest_path in enumerate(codebase_map["key_files"]["manifests"]): + # Filter Test-Bleed based on relative path + rel_path = os.path.relpath(manifest_path, app_dir) + normalized_rel_path = "/" + rel_path.replace("\\", "/") + if any( + x in normalized_rel_path + for x in [ + "/debug/", + "/test/", + "/androidTest/", + "/testFixtures/", + ] + ): + continue + + if os.path.exists(manifest_path): + try: + with open(manifest_path, "r") as f: + content = f.read() + if not manifest_content: + manifest_content = content + + details = extract_manifest_details(content) + + # Identity Fallbacks + if details.get("package_name") and not manifest_details.get( + "package_name" + ): + manifest_details["package_name"] = details["package_name"] + + if details.get("target_sdk") and not manifest_details.get( + "target_sdk" + ): + manifest_details["target_sdk"] = details["target_sdk"] + + if details.get("app_label") and not manifest_details.get( + "app_label" + ): + manifest_details["app_label"] = details["app_label"] + + manifest_details["permissions"].extend( + details.get("permissions", []) + ) + manifest_details["foreground_services"].extend( + details.get("foreground_services", []) + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to read manifest: {e}", file=sys.stderr) + + manifest_details["permissions"] = sorted( + list(set(manifest_details["permissions"])) + ) + + # Deduplicate and sort foreground services + unique_services = [] + seen_services = set() + for svc in manifest_details["foreground_services"]: + svc_key = (svc.get("name"), svc.get("type")) + if svc_key not in seen_services: + seen_services.add(svc_key) + unique_services.append(svc) + + # Final stable sort + manifest_details["foreground_services"] = sorted( + unique_services, + key=lambda x: (x.get("name") or "", x.get("type") or ""), + ) + + # 3. Gather results + data_safety_scan = scanner_future.result() + + # 5. Post-Scan Flavor Filtering + if prioritized_flavors: + data_safety_scan = _filter_by_flavor( + data_safety_scan, detected_flavors, prioritized_flavors + ) + + # 6. Ingest local Play Store info + local_info_path = os.path.join( + app_dir, "play_store_assets", "play_store_info.json" + ) + if os.path.exists(local_info_path): + try: + with open(local_info_path, "r", encoding="utf-8") as f: + play_store_info = json.load(f) + if "is_published" not in play_store_info: + play_store_info["is_published"] = True + if "store_url" not in play_store_info: + play_store_info["store_url"] = f"file://{local_info_path}" + + with open(os.path.join(temp_dir, "play_store_info.json"), "w") as out_f: + json.dump(play_store_info, out_f, indent=4) + print( + f"Ingested local Play Store info from {local_info_path}", + file=sys.stderr, + ) + except Exception as e: # pylint: disable=broad-exception-caught + print( + f"Warning: Failed to ingest local Play Store info: {e}", + file=sys.stderr, + ) + + reasoning = [] + goal_map = {} + resources_dir = os.path.join(repo_root, "resources") + if os.path.exists(resources_dir): + for f in os.listdir(resources_dir): + if f.startswith("goal_") and f.endswith(".md"): + goal_name = f[5:-3] + goal_map[goal_name] = {"prompt_file": f} + + if prioritized_flavors: + reasoning.append( + f"Prioritizing Play Store flavors: {', '.join(prioritized_flavors)}" + ) + reasoning.append("Activated via template evaluation") + + output_data = { + "app_name": ( + manifest_details.get("app_label") + or os.path.basename(os.path.normpath(app_dir)) + ), + "package_name": manifest_details.get("package_name"), + "target_dir": app_dir, + "codebase_map": codebase_map, + "manifest_content": manifest_content, + "manifest_details": manifest_details, + "application_modules": app_modules, + "data_safety_scan": data_safety_scan, + "triage_summary": generate_triage_summary( + manifest_details, data_safety_scan + ), + "triage_reasoning": "\n".join(reasoning), + "detected_flavors": sorted(list(detected_flavors)), + "prioritized_flavors": prioritized_flavors, + } + + try: + activated_goals = write_agent_prompts( + {"repo_root": repo_root, "temp_dir": temp_dir, "app_dir": app_dir}, + goal_map, + output_data, + ) + print(f"Prompts generated in {temp_dir}", file=sys.stderr) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Warning: Failed to generate prompts: {e}", file=sys.stderr) + + print( + json.dumps( + {"temp_dir": temp_dir, "activated_goals": activated_goals}, indent=4 + ) + ) + + +if __name__ == "__main__": + main() diff --git a/play/play-policy-insights/scripts/play_store_scraper.py b/play/play-policy-insights/scripts/play_store_scraper.py new file mode 100755 index 0000000..bab9bdc --- /dev/null +++ b/play/play-policy-insights/scripts/play_store_scraper.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 + +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Scrapes and parses app metadata and safety info from Play Store.""" + +import html.parser +import json +import os +import ssl +import sys +import urllib.error +import urllib.request + + +def fetch_html(url, verify_ssl=True): + """Fetches HTML content from a URL with a realistic User-Agent.""" + headers = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/119.0.0.0 Safari/537.36" + ), + "Accept-Language": "en-US,en;q=0.9", + } + req = urllib.request.Request(url, headers=headers) + if verify_ssl: + context = ssl.create_default_context() + else: + context = ssl._create_unverified_context() + try: + with urllib.request.urlopen(req, timeout=10, context=context) as resp: + return resp.read().decode("utf-8") + except urllib.error.HTTPError as e: + if e.code == 404: + print(f"App not found on Play Store (404): {url}", file=sys.stderr) + else: + print(f"HTTP Error: {e.code} {e.reason}", file=sys.stderr) + return None + except Exception as e: + print(f"Error fetching {url}: {e}", file=sys.stderr) + return None + + +class PlayStoreMetadataParser(html.parser.HTMLParser): + """Parses core app metadata from Play Store HTML.""" + + def __init__(self): + super().__init__() + self.ld_json = None + self.meta_category = None + self.meta_content_rating = None + self.meta_description = None + self.title_text = None + self.developer_name = None + self.privacy_policy_url = None + + self.in_ld_json = False + self.json_data = [] + self.in_title = False + self.in_dev_link = False + + self.in_a_tag = False + self.current_a_href = None + + def handle_starttag(self, tag, attrs): + attrs_dict = dict(attrs) + + if tag == "script" and attrs_dict.get("type") == "application/ld+json": + self.in_ld_json = True + self.json_data = [] + + if tag == "meta": + itemprop = attrs_dict.get("itemprop") + if itemprop == "applicationCategory": + self.meta_category = attrs_dict.get("content") + elif itemprop == "contentRating": + self.meta_content_rating = attrs_dict.get("content") + + name = attrs_dict.get("name") + prop = attrs_dict.get("property") + if prop == "og:description" or name == "description": + if not self.meta_description: + self.meta_description = attrs_dict.get("content") + + if tag == "h1": + self.in_title = True + + if tag == "a": + href = attrs_dict.get("href", "") + if "/store/apps/dev" in href: + self.in_dev_link = True + + self.in_a_tag = True + self.current_a_href = href + + def handle_endtag(self, tag): + if tag == "script" and self.in_ld_json: + self.in_ld_json = False + try: + self.ld_json = json.loads("".join(self.json_data)) + if isinstance(self.ld_json, list): + self.ld_json = self.ld_json[0] + except (json.JSONDecodeError, TypeError, IndexError): + pass + if tag == "h1": + self.in_title = False + if tag == "a": + self.in_dev_link = False + self.in_a_tag = False + self.current_a_href = None + + def handle_data(self, data): + if self.in_ld_json: + self.json_data.append(data) + elif self.in_title and not self.title_text: + self.title_text = data.strip() + elif self.in_dev_link and not self.developer_name: + self.developer_name = data.strip() + elif self.in_a_tag and self.current_a_href: + if "privacy policy" in data.lower(): + if "google.com" not in self.current_a_href.lower(): + if not self.privacy_policy_url: + self.privacy_policy_url = self.current_a_href + + +class PlayStoreDataSafetyParser(html.parser.HTMLParser): + """Extracts visible text content from HTML structurally.""" + + def __init__(self): + super().__init__() + self.text_parts = [] + self.ignore_tags = {"script", "style", "noscript", "svg", "path"} + self.in_ignored_tag = 0 + + def handle_starttag(self, tag, attrs): + if tag.lower() in self.ignore_tags: + self.in_ignored_tag += 1 + + def handle_endtag(self, tag): + if tag.lower() in self.ignore_tags: + self.in_ignored_tag = max(0, self.in_ignored_tag - 1) + + def handle_data(self, data): + if self.in_ignored_tag == 0: + cleaned = data.strip() + if cleaned: + self.text_parts.append(cleaned) + + +def load_taxonomy(): + """Loads the valid data safety taxonomy from policies.json.""" + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + policy_path = os.path.join(repo_root, "resources", "policies.json") + try: + with open(policy_path, "r") as f: + data = json.load(f) + taxonomy = data.get("data_safety_section", {}).get("taxonomy", {}) + return { + item["data_type"].lower() + for item in taxonomy.values() + if "data_type" in item + } + except Exception as e: + print( + f"Warning: Failed to load taxonomy from {policy_path}: {e}", + file=sys.stderr, + ) + return set() + + +# Official Play Store Data Safety Taxonomy for validation +VALID_TAXONOMY = load_taxonomy() + + +def extract_metadata(html_content): + """Extracts all metadata into a clean dictionary.""" + metadata = { + "title": "Unknown App", + "developer": "Unknown Developer", + "category": None, + "content_rating": None, + "description": None, + "privacy_policy_url": None, + } + + if not html_content: + return metadata + + parser = PlayStoreMetadataParser() + parser.feed(html_content) + + # 1. Prioritize ld+json payload + if parser.ld_json: + metadata["title"] = parser.ld_json.get("name", metadata["title"]) + + author = parser.ld_json.get("author", {}) + if isinstance(author, dict): + metadata["developer"] = author.get("name", metadata["developer"]) + elif isinstance(author, str): + metadata["developer"] = author + + metadata["category"] = parser.ld_json.get( + "applicationCategory", metadata["category"] + ) + metadata["content_rating"] = parser.ld_json.get( + "contentRating", metadata["content_rating"] + ) + metadata["description"] = parser.ld_json.get( + "description", metadata["description"] + ) + + # 2. Fall back to HTML tags if json was missing elements + if metadata["title"] == "Unknown App" and parser.title_text: + metadata["title"] = parser.title_text + if metadata["developer"] == "Unknown Developer" and parser.developer_name: + metadata["developer"] = parser.developer_name + if not metadata["category"] and parser.meta_category: + metadata["category"] = parser.meta_category + if not metadata["content_rating"] and parser.meta_content_rating: + metadata["content_rating"] = parser.meta_content_rating + if not metadata["description"] and parser.meta_description: + metadata["description"] = parser.meta_description + + # Privacy Policy is only found in tags + metadata["privacy_policy_url"] = parser.privacy_policy_url + + return metadata + + +def extract_data_safety(html_content): + """Extracts Data Safety information structurally.""" + if not html_content: + return {"data_collected": [], "data_shared": []} + + parser = PlayStoreDataSafetyParser() + parser.feed(html_content) + parts = parser.text_parts + + result = {"data_collected": [], "data_shared": []} + + purposes_keywords = [ + "App functionality", + "Analytics", + "Fraud prevention, security, and compliance", + "Personalization", + "Account management", + "Advertising or marketing", + "Developer communications", + ] + + known_categories = { + "Location", + "Personal info", + "Financial info", + "Health and fitness", + "Messages", + "Photos and videos", + "Audio files", + "Files and docs", + "Calendar", + "Contacts", + "App activity", + "Web browsing", + "App info and performance", + "Device or other IDs", + } + + # Robust parsing ignoring UI icons + cur_sec = None + cur_cat = None + cur_type = None + + for p in parts: + p_lower = p.lower() + + # Section Transitions + if ( + p_lower == "data shared" + or p_lower + == "here's more information the developer has provided about the kinds" + " of data this app may share" + ): + cur_sec = "data_shared" + cur_cat = None + continue + elif ( + p_lower == "data collected" + or p_lower + == "here's more information the developer has provided about the kinds" + " of data this app may collect" + ): + cur_sec = "data_collected" + cur_cat = None + continue + elif p_lower == "security practices" or p_lower == "data safety": + if cur_sec: + break + + if not cur_sec: + continue + + # Ignore UI artifacts + if p_lower in [ + "expand_more", + "expand_less", + "info", + "data collected and for what purpose", + "data shared and for what purpose", + ]: + continue + + # If it's a known purpose keyword, attach it to the current type + matched_purposes = [kw for kw in purposes_keywords if kw.lower() in p_lower] + if matched_purposes: + if cur_type: + cur_type["purposes"].extend(matched_purposes) + cur_type["purposes"] = list(set(cur_type["purposes"])) + continue + + if p_lower == "· optional": + if cur_type: + cur_type["optional"] = True + continue + + # If it's not a purpose or an artifact, it's either a Category or a + # Data Type + if len(p) > 50: + continue + + if p in known_categories: + if not cur_cat or cur_cat["category"] != p: + cur_cat = {"category": p, "types": []} + result[cur_sec].append(cur_cat) + cur_type = None + continue + + if cur_cat: + if p == cur_cat["category"]: + continue + + cur_type = {"type": p, "purposes": [], "optional": False} + cur_cat["types"].append(cur_type) + + # Post-process to remove summary headers (UI groupings) + for cat in result["data_collected"] + result["data_shared"]: + filtered_types = [] + cat_name = cat["category"].lower() + + for t in cat["types"]: + t_name_lower = t["type"].lower() + + # Indicators that this is a summary header rather than a declaration: + # 1. It has no associated purposes (headers are never assigned purposes) + # 2. AND it either: + # a. Matches the category name exactly (redundant UI artifact) + # b. Is not a valid item in the official taxonomy (summary string) + if not t["purposes"]: + if ( + t_name_lower == cat_name + or t_name_lower not in VALID_TAXONOMY + ): + continue + + filtered_types.append(t) + + cat["types"] = filtered_types + + return result + + +def scrape_app_details(pkg_name, verify_ssl=True): + """Scrapes app metadata and Data Safety info for a given package.""" + details_url = ( + f"https://play.google.com/store/apps/details?id={pkg_name}&hl=en&gl=US" + ) + ds_url = ( + f"https://play.google.com/store/apps/datasafety?id={pkg_name}&hl=en&gl=US" + ) + + details_html = fetch_html(details_url, verify_ssl=verify_ssl) + metadata = extract_metadata(details_html) + + ds_html = fetch_html(ds_url, verify_ssl=verify_ssl) + data_safety = extract_data_safety(ds_html) + + return { + "package_name": pkg_name, + "is_published": bool(details_html), + "store_url": details_url, + "title": metadata["title"], + "developer": metadata["developer"], + "description": metadata["description"], + "category": metadata["category"], + "app_info": {"content_rating": metadata["content_rating"]}, + "developer_links": {"privacy_policy_url": metadata["privacy_policy_url"]}, + "data_safety": data_safety, + } diff --git a/play/play-policy-insights/scripts/scanner.py b/play/play-policy-insights/scripts/scanner.py new file mode 100755 index 0000000..ec5305f --- /dev/null +++ b/play/play-policy-insights/scripts/scanner.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 + +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Scans Android codebase for data safety signals and sensitive API usage.""" + +import concurrent.futures +import functools +import json +import os +import re +import sys + +# Pre-compiled regex patterns at module scope for thread-safe performance +URL_PATTERN = re.compile(r'"(https?://[^"]+)"') +XML_COMMENT_PATTERN = re.compile(r"", flags=re.DOTALL) +COMMENT_STRIP_PATTERN = re.compile( + r'("(?:\\.|[^"\\])*")|//[^\n]*|/\*.*?\*/', + flags=re.DOTALL, +) + + +@functools.lru_cache(maxsize=1) +def get_scanner_config(): + """Loads the scanner configuration from scanner_config.json with caching.""" + config_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "resources", + "scanner_config.json", + ) + try: + with open(config_path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print( + f"Warning: Failed to load scanner config from {config_path}: {e}", + file=sys.stderr, + ) + return {} + + +@functools.lru_cache(maxsize=1) +def get_supported_extensions(): + """Returns a tuple of supported file extensions from config.""" + return tuple(get_scanner_config().get("supported_extensions", [])) + + +def collect_target_files(target_dir): + """Performs a single master walk to categorize files.""" + categorized = { + "manifests": [], + "gradles": [], + "source_code": [], + "all_files": [], + } + + normalized_target = os.path.abspath(target_dir) + hybrid_root = normalized_target + target_parent = os.path.dirname(normalized_target) + + for potential_root in [normalized_target, target_parent]: + if os.path.exists( + os.path.join(potential_root, "package.json") + ) or os.path.exists(os.path.join(potential_root, "pubspec.yaml")): + hybrid_root = potential_root + print( + f"Hybrid app detected. Expanding scan root to: {hybrid_root}", + file=sys.stderr, + ) + break + + config = get_scanner_config() + ignored_dirs = set(config.get("ignored_directories", [])) + supported_exts = get_supported_extensions() + + for root, dirs, files in os.walk(hybrid_root): + dirs[:] = sorted([d for d in dirs if d not in ignored_dirs]) + for file in sorted(files): + file_path = os.path.join(root, file) + categorized["all_files"].append((file, file_path, root)) + + # Surgical filter for XMLs to avoid scanning thousands of low-signal + # assets. + if file.endswith(".xml"): + # Exclude test-bleed XMLs + if not any( + x in file_path.lower() + for x in ["/debug/", "/test/", "/androidtest/", "/testfixtures/"] + ): + if file == "AndroidManifest.xml": + categorized["manifests"].append(file_path) + else: + path_parts = file_path.split(os.sep) + is_valuable_xml = False + for i, part in enumerate(path_parts): + if part in ["res", "resources"] and i + 1 < len(path_parts): + next_part = path_parts[i + 1] + if any( + next_part.startswith(d) + for d in ["layout", "values", "xml", "navigation"] + ): + is_valuable_xml = True + break + if is_valuable_xml: + categorized["source_code"].append(file_path) + + elif file.lower().endswith((".gradle", ".gradle.kts")): + categorized["gradles"].append(file_path) + + elif file.lower().endswith(supported_exts) or ( + file.lower() == "pubspec.yaml" or file.lower() == "package.json" + ): + # Filter (Gap 3 - OS-Specific Path Separators and Test-Bleed based on + # filename/path) + file_lower = file.lower() + is_test_or_debug = ( + any( + x in file_path.lower() + for x in [ + "/debug/", + "/test/", + "/androidtest/", + "/testfixtures/", + ".spec.js", + ".spec.ts", + ".spec.jsx", + ".spec.tsx", + ] + ) + or file_lower.endswith("_test.dart") + ) + if not is_test_or_debug: + categorized["source_code"].append(file_path) + + return categorized + + +def _identify_semantic_files(all_files): + """Maps files to semantic categories based on filename patterns.""" + semantic_files = {} + file_patterns = get_scanner_config().get("file_patterns", {}) + supported_exts = get_supported_extensions() + + for file, file_path, root in all_files: + file_lower = file.lower() + # Check if it's a relevant source/config file + if file_lower.endswith(supported_exts + (".xml",)): + rel_path = os.path.relpath(file_path, root) + for category, patterns in file_patterns.items(): + if any(p in file_lower for p in patterns): + if category not in semantic_files: + semantic_files[category] = [] + semantic_files[category].append(rel_path) + + return semantic_files + + +def _scan_single_file(file_path, all_signal_categories, target_dir): + """Internal worker to scan a single file for all signal categories.""" + local_api_usage = {} + found_urls = [] + supported_exts = get_supported_extensions() + + try: + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + # URLs - with quick check for policy relatedness + policy_url_keywords = ["privacy", "support", "delete", "terms", "policy"] + for match in URL_PATTERN.finditer(content): + url = match.group(1) + if any(kw in url.lower() for kw in policy_url_keywords): + found_urls.append(url) + + # Cleanup comments based on file type + file_path_lower = file_path.lower() + if file_path_lower.endswith(".xml"): + content = XML_COMMENT_PATTERN.sub("", content) + elif file_path_lower.endswith(supported_exts): + content = COMMENT_STRIP_PATTERN.sub( + lambda m: m.group(1) if m.group(1) else "", content + ) + + # Signal Patterns + for cat_name, signals in all_signal_categories.items(): + for signal_id, patterns in signals.items(): + for p in patterns: + if p in content: + if cat_name not in local_api_usage: + local_api_usage[cat_name] = {} + if signal_id not in local_api_usage[cat_name]: + local_api_usage[cat_name][signal_id] = [] + + rel_path = os.path.relpath(file_path, target_dir) + local_api_usage[cat_name][signal_id].append( + f"{rel_path} (Pattern: {p})" + ) + break # Move to next signal once pattern found in file + + except Exception: # pylint: disable=broad-exception-caught + pass + + return {"api_usage": local_api_usage, "urls": found_urls} + + +def perform_scan(target_dir, file_inventory): + """Orchestrates the parallel scanning of identified files.""" + api_usage = {} + all_urls = [] + all_signal_categories = get_scanner_config().get("signal_categories", {}) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + futures = [ + executor.submit( + _scan_single_file, f, all_signal_categories, target_dir + ) + for f in file_inventory.get("source_code", []) + ] + + for future in concurrent.futures.as_completed(futures): + res = future.result() + for cat_name, cat_findings in res["api_usage"].items(): + if cat_name not in api_usage: + api_usage[cat_name] = {} + for signal_id, findings in cat_findings.items(): + if signal_id not in api_usage[cat_name]: + api_usage[cat_name][signal_id] = [] + api_usage[cat_name][signal_id].extend(findings) + + for u in res["urls"]: + all_urls.append(u) + + # Final cleanup and formatting + for cat_name in api_usage: + for signal_id in api_usage[cat_name]: + api_usage[cat_name][signal_id] = sorted( + list(set(api_usage[cat_name][signal_id])) + ) + + return { + "data_safety_scan": api_usage, + "found_urls": sorted(list(set(all_urls))), + "semantic_files": _identify_semantic_files( + file_inventory.get("all_files", []) + ), + "codebase_map": file_inventory, + } diff --git a/play/play-policy-insights/scripts/template_engine.py b/play/play-policy-insights/scripts/template_engine.py new file mode 100644 index 0000000..1c4fccb --- /dev/null +++ b/play/play-policy-insights/scripts/template_engine.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Small logic-less template engine for Markdown generation.""" + +import re + +# Precompiled regex patterns at module scope for linear-time optimizations +TAG_PATTERN = re.compile(r"\{\{(#|/)?(IF_ANY|IF_ALL|IF|EACH)?\s*(.*?)\}\}") +NEWLINE_COLLAPSE_PATTERN = re.compile(r"\n{3,}") +VAR_VALID_PATTERN = re.compile(r"^[A-Za-z0-9_.]+$") + + +def get_nested_val(ctx, key): + """Resolves nested dot-notation keys from context (e.g., app.info.package).""" + parts = key.split(".") + val = ctx + for p in parts: + if isinstance(val, dict): + val = val.get(p) + else: + return None + return val + + +class Node: + """Base class for all Abstract Syntax Tree (AST) node elements.""" + + def render(self, ctx, buffer): + raise NotImplementedError + + +class TextNode(Node): + """Represents raw static text blocks.""" + + def __init__(self, text): + self.text = text + + def render(self, ctx, buffer): + buffer.append(self.text) + + +class VarNode(Node): + """Represents dynamic variable interpolation with literal fallbacks.""" + + def __init__(self, key, raw_text): + self.key = key + self.raw_text = raw_text + + def render(self, ctx, buffer): + val = get_nested_val(ctx, self.key) + if val is not None: + buffer.append(str(val)) + else: + # Preserve the unresolved tag exactly like the original engine + buffer.append(self.raw_text) + + +class IfNode(Node): + """Represents conditional block logic (IF, IF_ALL, IF_ANY).""" + + def __init__(self, keys_str, mode, children): + self.keys = [k.strip() for k in keys_str.split(",")] + self.mode = mode + self.children = children + + def render(self, ctx, buffer): + condition_met = False + if self.mode == "IF": + condition_met = bool(get_nested_val(ctx, self.keys[0])) + elif self.mode == "IF_ANY": + condition_met = any(get_nested_val(ctx, k) for k in self.keys) + elif self.mode == "IF_ALL": + condition_met = all(get_nested_val(ctx, k) for k in self.keys) + + if condition_met: + for child in self.children: + child.render(ctx, buffer) + + +def strip_nodes(nodes): + """Helper to strip leading/trailing whitespace from first/last TextNodes.""" + if not nodes: + return nodes + + # Strip leading whitespace from first TextNode + if isinstance(nodes[0], TextNode): + nodes[0].text = nodes[0].text.lstrip() + if not nodes[0].text: + nodes.pop(0) + + # Strip trailing whitespace from last TextNode + if nodes and isinstance(nodes[-1], TextNode): + nodes[-1].text = nodes[-1].text.rstrip() + if not nodes[-1].text: + nodes.pop() + + return nodes + + +class EachNode(Node): + """Represents standard iteration logic {{#EACH}} over lists/dicts.""" + + def __init__(self, key, children): + self.key = key + self.children = strip_nodes(children) + + def _render_children(self, local_ctx): + """Helper to render nested children under a specific loop context.""" + sub_buffer = [] + for child in self.children: + child.render(local_ctx, sub_buffer) + return "".join(sub_buffer) + + def render(self, ctx, buffer): + val = get_nested_val(ctx, self.key) + if not val: + return + + rendered_items = [] + if isinstance(val, dict): + for k, v in val.items(): + if k == "-": + continue + new_ctx = ctx.copy() + new_ctx["KEY"] = str(k) + new_ctx["VALUE"] = v + rendered_items.append(self._render_children(new_ctx)) + + elif isinstance(val, list): + for item in val: + new_ctx = ctx.copy() + new_ctx["ITEM"] = item + rendered_items.append(self._render_children(new_ctx)) + + # Join iterations together with single newline + buffer.append("\n".join(rendered_items)) + + +class _ParserFrame: + """Helper class representing an active stack frame in the compiler.""" + + def __init__(self, tag_type, children, match): + self.tag_type = tag_type + self.children = children + self.match = match + + +def compile_template(template_str): + """Parses template_str forward-only, building the AST Node tree.""" + root_children = [] + stack = [_ParserFrame(None, root_children, None)] + + pos = 0 + for match in TAG_PATTERN.finditer(template_str): + start, end = match.span() + + # 1. Capture static text prior to the tag + if start > pos: + stack[-1].children.append(TextNode(template_str[pos:start])) + + prefix = match.group(1) + tag_type = match.group(2) + arg = match.group(3).strip() if match.group(3) else "" + raw_tag = match.group(0) + + if prefix == "#": + if tag_type: + # Pushing a new nested frame block onto the stack + new_children = [] + stack.append(_ParserFrame(tag_type, new_children, match)) + else: + # Invalid tag starting with #, treat as normal text block + stack[-1].children.append(TextNode(raw_tag)) + + elif prefix == "/": + if tag_type: + # Block closer tag + if len(stack) > 1 and stack[-1].tag_type == tag_type: + frame = stack.pop() + open_arg = frame.match.group(3).strip() + + if frame.tag_type == "EACH": + node = EachNode(open_arg, frame.children) + else: + node = IfNode(open_arg, frame.tag_type, frame.children) + + stack[-1].children.append(node) + else: + # Mismatched closer, treat as static text + stack[-1].children.append(TextNode(raw_tag)) + else: + # Invalid closer, treat as static text + stack[-1].children.append(TextNode(raw_tag)) + + else: + # Variable tag or unknown braces + if VAR_VALID_PATTERN.match(arg): + stack[-1].children.append(VarNode(arg, raw_tag)) + else: + stack[-1].children.append(TextNode(raw_tag)) + + pos = end + + # Capture any trailing text blocks + if pos < len(template_str): + stack[-1].children.append(TextNode(template_str[pos:])) + + # Graceful recovery for unclosed tags to prevent runtime crashes + while len(stack) > 1: + frame = stack.pop() + # Re-inject the unclosed opening tag as plain text, followed by its children + stack[-1].children.append(TextNode(frame.match.group(0))) + stack[-1].children.extend(frame.children) + + return root_children + + +def render_template(template_str, context_dict): + """Renders a markdown template with high performance and zero dependencies.""" + nodes = compile_template(template_str) + + buffer = [] + for node in nodes: + node.render(context_dict, buffer) + + final_string = "".join(buffer) + + # Collapse 3 or more consecutive newlines into exactly 2 (preserves layout) + return NEWLINE_COLLAPSE_PATTERN.sub("\n\n", final_string) From 47e1dff74a5cde5d0128c5d15e74e000323135ea Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Tue, 14 Jul 2026 08:21:32 +0000 Subject: [PATCH 32/61] Updates skills (2026-07-14 08:21) --- play/play-policy-insights/SKILL.md | 2 +- .../resources/compliance_report_template.md | 7 ++++--- .../resources/goal_permissions_and_apis.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/play/play-policy-insights/SKILL.md b/play/play-policy-insights/SKILL.md index a562751..8eeb9d1 100644 --- a/play/play-policy-insights/SKILL.md +++ b/play/play-policy-insights/SKILL.md @@ -4,7 +4,7 @@ description: Automated auditor designed to verify Android applications against G license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-10' + last-updated: '2026-07-13' keywords: - account deletion - accessibility api diff --git a/play/play-policy-insights/resources/compliance_report_template.md b/play/play-policy-insights/resources/compliance_report_template.md index e9854c9..e045394 100644 --- a/play/play-policy-insights/resources/compliance_report_template.md +++ b/play/play-policy-insights/resources/compliance_report_template.md @@ -6,7 +6,8 @@ **informational and advisory purposes only**. It does NOT constitute legal advice, a guarantee of Play Store approval, or a definitive compliance check. The Google Play Review team has final authority. Undiscovered issues may still - exist. + exist. The developer retains sole responsibility and liability for the apps’ + security, data privacy compliance, and final Play Store submission. --- @@ -44,7 +45,7 @@ Follow these personalized steps to resolve the identified risks. | Overall Status | Meaning & Rejection Risk | Required Action | | :--- | :--- | :--- | -| **🟢 Compliant** | The application code and metadata appear fully compliant with analyzed Play Store policies. No active policy violations or high-risk discrepancies were identified. | **Safe to Submit**: Safe for Play Store submission. | +| **🟢 Compliant** | The application code and metadata appear fully compliant with analyzed Play Store policies. No active policy violations or high-risk discrepancies were identified. | **No issues detected**: No active policy violations were identified by this automatic scan. | | **🟡 Needs Review** | No critical violations were found in the code, but the app implements features requiring manual Play Console configurations (e.g., submitting **Reviewer Demo Credentials** for login screens) or requests restricted, high-risk permissions (e.g., Exact Alarms, All Files Access, or Background Location) requiring Play Console declaration forms. | **Must Review**: Review the findings. You must either **migrate to modern scoped alternatives (highly recommended)** to bypass declaration scrutiny, OR **manually verify that your Play Console settings (like Reviewer Credentials and Declaration Forms) are completely and carefully configured** before submitting. | | **🔴 Non-Compliant** | Severe, direct policy violations (e.g., silent background data tracking, default-only permission abuse) or active Data Safety declaration mismatches were identified. | **Must Fix**: Immediate rejection or account-level policy suspension risk. You must remediate these issues before submitting to the Play Store. | @@ -52,7 +53,7 @@ Follow these personalized steps to resolve the identified risks. | Finding Severity | Description | Action Required | | :--- | :--- | :--- | -| **🔴 Critical** | Direct, verified policy violation (e.g., unauthorized data collection/sharing). | **Must Fix**: Immediate rejection risk. | +| **🔴 Critical** | Direct, verified policy violation (e.g., unauthorized data collection/sharing). | Critical action recommended. | | **🟡 Important** | Deprecated permission use or API discrepancy where scoped pathways are mandated. | **Should Fix**: Highly recommended to migrate to prevent manual reviewer flags. | | **🔵 Suggestion** | Standard advisory reminder or administrative checklist item (fully compliant). | **Good to Know**: Clean-path confirmation or helper checklist. | diff --git a/play/play-policy-insights/resources/goal_permissions_and_apis.md b/play/play-policy-insights/resources/goal_permissions_and_apis.md index f933005..126d95e 100644 --- a/play/play-policy-insights/resources/goal_permissions_and_apis.md +++ b/play/play-policy-insights/resources/goal_permissions_and_apis.md @@ -290,8 +290,8 @@ purpose", use these fast heuristics: - **Common Evaluation Matrix**: | Service Configuration | Real Code Usage | Justified? | Severity | Direct Actionable Recommendation | | :--- | :--- | :--- | :--- | :--- | - | **`isAccessibilityTool="true"`** | Code performs screen scraping, ad blocking, or automated click routines for standard users. | No (Violation) | `CRITICAL` | **Immediate Rejection Risk**: Remove accessibility helper configs. Migrate UI automation to standard Android testing libraries. | - | **`isAccessibilityTool` false/missing** | Code implements an accessibility listener but is not a dedicated helper app. | No prominent disclosure | `CRITICAL` | **Immediate Rejection Risk**: Because you request accessibility permission for a standard utility, you MUST implement an in-app **Prominent Disclosure and Affirmative Consent screen** before asking the user to enable the service, otherwise your app will be rejected. | + | **`isAccessibilityTool="true"`** | Code performs screen scraping, ad blocking, or automated click routines for standard users. | No (Violation) | `CRITICAL` | **High Risk of Play Store Rejection**: Remove accessibility helper configs. Migrate UI automation to standard Android testing libraries. | + | **`isAccessibilityTool` false/missing** | Code implements an accessibility listener but is not a dedicated helper app. | No prominent disclosure | `CRITICAL` | **High Risk of Play Store Rejection**: Because you request accessibility permission for a standard utility, you MUST implement an in-app **Prominent Disclosure and Affirmative Consent screen** before asking the user to enable the service, otherwise your app will be rejected. | - **Domain-Specific Heuristics (Strictly Bounded)**: Trace the ingestion of accessibility events: From bc6cd7246a4b834ee8dcb4cc99ce6f17ee23f757 Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:56:32 +0000 Subject: [PATCH 33/61] Add Claude Code and Codex plugin/marketplace manifests (#111) --- .agents/plugins/marketplace.json | 21 +++++ .claude-plugin/marketplace.json | 42 ++++++++++ .codex-plugin/plugin.json | 134 +++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .codex-plugin/plugin.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..c2d27f9 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,21 @@ +{ + "name": "android-skills", + "interface": { + "displayName": "Android Skills" + }, + "plugins": [ + { + "name": "android-skills", + "source": { + "source": "url", + "url": "https://github.com/android/skills", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} \ No newline at end of file diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..b217d93 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,42 @@ +{ + "name": "android-skills", + "owner": { + "name": "Google / Android" + }, + "plugins": [ + { + "name": "android-skills", + "description": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.", + "author": { + "name": "Google / Android" + }, + "category": "development", + "source": "./", + "strict": false, + "skills": [ + "./build/agp/agp-9-upgrade", + "./camera/camerax", + "./device-ai/appfunctions", + "./devtools/android-cli", + "./identity/verified-email", + "./jetpack-compose/adaptive", + "./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose", + "./jetpack-compose/theming/styles", + "./navigation/navigation-3", + "./performance/r8-analyzer", + "./play/engage-sdk-integration", + "./play/play-billing-library-version-upgrade", + "./play/play-policy-insights", + "./profilers/perfetto-sql", + "./profilers/perfetto-trace-analysis", + "./security/android-intent-security", + "./system/edge-to-edge", + "./testing/testing-setup", + "./wear/jetpack-compose-m3", + "./xr/display-glasses-with-jetpack-compose-glimmer" + ], + "homepage": "https://github.com/android/skills", + "license": "Apache-2.0" + } + ] +} \ No newline at end of file diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..d83d925 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,134 @@ +{ + "name": "android-skills", + "version": "1.0.5", + "description": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.", + "author": { + "name": "Google / Android", + "url": "https://github.com/android" + }, + "repository": "https://github.com/android/skills", + "license": "Apache-2.0", + "keywords": [ + "android", + "kotlin", + "jetpack-compose", + "skills" + ], + "skills": [ + { + "source": { + "path": "./build/agp/agp-9-upgrade" + } + }, + { + "source": { + "path": "./camera/camerax" + } + }, + { + "source": { + "path": "./device-ai/appfunctions" + } + }, + { + "source": { + "path": "./devtools/android-cli" + } + }, + { + "source": { + "path": "./identity/verified-email" + } + }, + { + "source": { + "path": "./jetpack-compose/adaptive" + } + }, + { + "source": { + "path": "./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose" + } + }, + { + "source": { + "path": "./jetpack-compose/theming/styles" + } + }, + { + "source": { + "path": "./navigation/navigation-3" + } + }, + { + "source": { + "path": "./performance/r8-analyzer" + } + }, + { + "source": { + "path": "./play/engage-sdk-integration" + } + }, + { + "source": { + "path": "./play/play-billing-library-version-upgrade" + } + }, + { + "source": { + "path": "./play/play-policy-insights" + } + }, + { + "source": { + "path": "./profilers/perfetto-sql" + } + }, + { + "source": { + "path": "./profilers/perfetto-trace-analysis" + } + }, + { + "source": { + "path": "./security/android-intent-security" + } + }, + { + "source": { + "path": "./system/edge-to-edge" + } + }, + { + "source": { + "path": "./testing/testing-setup" + } + }, + { + "source": { + "path": "./wear/jetpack-compose-m3" + } + }, + { + "source": { + "path": "./xr/display-glasses-with-jetpack-compose-glimmer" + } + } + ], + "interface": { + "displayName": "Android Skills", + "shortDescription": "AI-optimized Android development skills from Google.", + "longDescription": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.", + "developerName": "Google / Android", + "category": "Developer Tools", + "capabilities": [ + "Skills" + ], + "websiteURL": "https://github.com/android/skills", + "defaultPrompt": [ + "Help me upgrade my app to AGP 9.", + "Migrate this Camera1 code to CameraX." + ] + } +} \ No newline at end of file From 4e1674995b166427c6e55c72bbd0d87b46d146db Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Wed, 29 Jul 2026 08:28:58 +0000 Subject: [PATCH 34/61] Updates skills (2026-07-29 08:28) --- .../agp/agp-9-upgrade/SKILL.md | 2 +- .../build/migrate-to-built-in-kotlin.md | 0 .../build/releases/agp-9-0-0-release-notes.md | 3 + .../agp-9-upgrade/references/buildconfig.md | 2 +- .../agp/agp-9-upgrade/references/ksp-kapt.md | 0 .../references/paparazzi-gradle-9.md | 0 .../agp/agp-9-upgrade/references/recipes.md | 2 +- device-ai/appfunctions/SKILL.md | 34 +- device-ai/appfunctions/context.md | 26 + .../references/adb-interaction-testing.md | 72 +- .../references/feature-discovery-analysis.md | 63 +- .../implementation-configuration.md | 327 +++++--- .../kdoc-refinement-optimization.md | 68 +- .../migrate-to-service-entry-point.md | 226 ++++++ devtools/android-cli/SKILL.md | 2 +- devtools/android-cli/references/interact.md | 2 +- navigation/navigation-3/SKILL.md | 4 +- .../navigation-3/migration-guide.md | 14 +- .../navigation-3/recipes/deeplinks-basic.md | 744 ++++++++++++++++++ performance/r8-analyzer/SKILL.md | 2 +- .../enable-app-optimization.md | 138 ++-- play/engage-sdk-integration/SKILL.md | 86 +- .../android/guide/playcore/engage/social.md | 702 ----------------- .../references/clusters.md | 4 +- .../references/patterns.md | 83 +- .../references/requests.md | 17 +- .../references/schemas/food.md | 2 +- .../references/schemas/listen.md | 2 +- .../references/schemas/other.md | 2 +- .../references/schemas/read.md | 2 +- .../references/schemas/shopping.md | 2 +- .../references/schemas/social.md | 2 +- .../references/schemas/travel.md | 2 +- .../references/schemas/tv.md | 2 +- .../references/schemas/watch.md | 2 +- wear/wear-compose-m3/SKILL.md | 2 +- .../wearables/compose/migrate-to-material3.md | 10 +- .../SKILL.md | 14 +- .../references/deptheffect-source.md | 11 +- .../references/glimmertheme-source.md | 9 +- .../references/list-source.md | 393 +-------- .../references/liststate-source.md | 67 +- 42 files changed, 1637 insertions(+), 1510 deletions(-) rename {build => build-system}/agp/agp-9-upgrade/SKILL.md (99%) rename {build => build-system}/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md (100%) rename {build => build-system}/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md (99%) rename {build => build-system}/agp/agp-9-upgrade/references/buildconfig.md (90%) rename {build => build-system}/agp/agp-9-upgrade/references/ksp-kapt.md (100%) rename {build => build-system}/agp/agp-9-upgrade/references/paparazzi-gradle-9.md (100%) rename {build => build-system}/agp/agp-9-upgrade/references/recipes.md (96%) create mode 100644 device-ai/appfunctions/context.md create mode 100644 device-ai/appfunctions/references/migrate-to-service-entry-point.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md delete mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/social.md diff --git a/build/agp/agp-9-upgrade/SKILL.md b/build-system/agp/agp-9-upgrade/SKILL.md similarity index 99% rename from build/agp/agp-9-upgrade/SKILL.md rename to build-system/agp/agp-9-upgrade/SKILL.md index 280347a..247b6ee 100644 --- a/build/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-06-25' + last-updated: '2026-07-23' keywords: - Android Gradle Plugin 9 - AGP 9 diff --git a/build/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 similarity index 100% rename from build/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md rename to build-system/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md diff --git a/build/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 similarity index 99% rename from build/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md rename to build-system/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md index e6d3dbe..d092353 100644 --- a/build/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 @@ -104,6 +104,9 @@ 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). + 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. diff --git a/build/agp/agp-9-upgrade/references/buildconfig.md b/build-system/agp/agp-9-upgrade/references/buildconfig.md similarity index 90% rename from build/agp/agp-9-upgrade/references/buildconfig.md rename to build-system/agp/agp-9-upgrade/references/buildconfig.md index 273f876..e4e19b6 100644 --- a/build/agp/agp-9-upgrade/references/buildconfig.md +++ b/build-system/agp/agp-9-upgrade/references/buildconfig.md @@ -37,7 +37,7 @@ 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/agp/agp-9-upgrade/references/recipes) +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 diff --git a/build/agp/agp-9-upgrade/references/ksp-kapt.md b/build-system/agp/agp-9-upgrade/references/ksp-kapt.md similarity index 100% rename from build/agp/agp-9-upgrade/references/ksp-kapt.md rename to build-system/agp/agp-9-upgrade/references/ksp-kapt.md diff --git a/build/agp/agp-9-upgrade/references/paparazzi-gradle-9.md b/build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md similarity index 100% rename from build/agp/agp-9-upgrade/references/paparazzi-gradle-9.md rename to build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md diff --git a/build/agp/agp-9-upgrade/references/recipes.md b/build-system/agp/agp-9-upgrade/references/recipes.md similarity index 96% rename from build/agp/agp-9-upgrade/references/recipes.md rename to build-system/agp/agp-9-upgrade/references/recipes.md index 5cf4a2d..d3ecc74 100644 --- a/build/agp/agp-9-upgrade/references/recipes.md +++ b/build-system/agp/agp-9-upgrade/references/recipes.md @@ -38,7 +38,7 @@ Additional details for each use-case follow. ### Add custom BuildConfig fields -See the detailed guide at [BuildConfig](https://developer.android.com/agents/skills/build/agp/agp-9-upgrade/references/buildconfig). +See the detailed guide at [BuildConfig](https://developer.android.com/agents/skills/build-system/agp/agp-9-upgrade/references/buildconfig). ### Renaming an APK diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md index 84ac2b2..ad75915 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-05-16' + last-updated: '2026-07-28' keywords: - AppFunctions - Kotlin @@ -36,30 +36,36 @@ understand and use the provided functionality. 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, as it handles backwards compatibility. +Always use the Jetpack library because it handles backward compatibility. ## Workflows -This skill enables the caller to discover, features that will be provided to +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: -- *[Step 1: Discovery](references/feature-discovery-analysis.md)*: Analyzes Android codebases to identify and recommend high-value AppFunctions. Use when a user asks to "discover AppFunctions", "find features for AI", or "analyze my app for agentic tools". -- *[Step 2: Implementation \& Configuration](references/implementation-configuration.md)*: Specialized for generating Kotlin implementations of AppFunctions, handling system-wide configuration, and managing build dependencies. Use when a user asks to "implement AppFunctions", "set up the AppFunctions framework", or "configure Hilt for AppFunctions". -- *[Step 3: KDoc Refinement](references/kdoc-refinement-optimization.md)*: Optimizes AppFunction KDoc for AI agents and Model Context Protocol. Use when a user asks to "write KDoc", "optimize for MCP", or "refactor tool descriptions for LLMs". -- *[Step 4: Testing \& Debugging](references/adb-interaction-testing.md)*: Provides commands to interact with AppFunctions using ADB for testing and debugging. Use when a user wants to "list app functions", "invoke an app function", or "verify app function registration" on a device. +- *[Step 1: Discovery](references/feature-discovery-analysis.md)*: Analyze Android codebases to identify and recommend potential AppFunctions. Use this step when a user asks to "discover AppFunctions," "find features for AI," or "analyze my app for agentic tools." +- *[Step 2: Implementation and configuration](references/implementation-configuration.md)*: Generate Kotlin implementations of AppFunctions, manage system-wide configuration, and configure build dependencies. Use this step when a user asks to "implement AppFunctions," "set up the AppFunctions framework," or "configure Hilt for AppFunctions." +- *[Step 3: KDoc refinement](references/kdoc-refinement-optimization.md)*: Optimize AppFunction KDoc for AI agents and Model Context Protocol. If a user asks to "write KDoc", "optimize for MCP", or "refactor tool descriptions for LLMs", use this step. +- *[Step 4: Testing and debugging](references/adb-interaction-testing.md)*: Provide commands to interact with AppFunctions using ADB for testing and debugging. If a user wants to "list app functions", "invoke an app function", or "verify app function registration" on a device, use this step. -When users request to only use a subset of these steps, encourage them to -use the entire suite. +If users request a subset of steps, you must encourage them to use all steps. -## Critical Constraints +If they apply, you must load the following references: -- **Modular Consistency** : Always ensure that implementations generated using [Implementation \& Configuration](references/implementation-configuration.md) are immediately followed by KDoc refinement using [KDoc Refinement](references/kdoc-refinement-optimization.md) to ensure maximum agent compatibility. -- **Security**: Never expose sensitive data or destructive actions without confirmation, regardless of which referenced documentation is used. +- *[Context and terminology](context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. +- *[Migration to service entry point](references/migrate-to-service-entry-point.md)* : Documents the systematic procedure for migrating Android applications using AppFunctions versions 1.0.0-alpha09 and lower to `AppFunctionServiceEntryPoint` architecture introduced in version 1.0.0-alpha10. Load when a user asks to migrate or upgrade existing AppFunctions code, or when you encounter legacy `AppFunctionConfiguration.Provider` implementations. + +## Critical constraints + +- **Modular consistency**: You must refine KDocs immediately after you generate AppFunction implementations. +- **Security**: Don't expose sensitive data or run destructive actions without user confirmation. ## Troubleshooting -- For build-time errors (KSP, Metadata), refer to [Implementation \& Configuration](references/implementation-configuration.md). -- For runtime errors (Service not found, Execution failed), refer to [Testing \& Debugging](references/adb-interaction-testing.md). +- If you encounter build-time errors such as Kotlin Symbol Processing (KSP) issues, see [Implementation and configuration](references/implementation-configuration.md). +- If you encounter runtime errors such as missing services or execution failures, see [Testing and debugging](references/adb-interaction-testing.md). +- If you need architecture definitions and vocabulary, see [Context and terminology](context.md). +- If you encounter issues when upgrading legacy configurations, see [Migration to service entry point](references/migrate-to-service-entry-point.md). diff --git a/device-ai/appfunctions/context.md b/device-ai/appfunctions/context.md new file mode 100644 index 0000000..dc49d25 --- /dev/null +++ b/device-ai/appfunctions/context.md @@ -0,0 +1,26 @@ +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. + +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 diff --git a/device-ai/appfunctions/references/adb-interaction-testing.md b/device-ai/appfunctions/references/adb-interaction-testing.md index 9ebc738..b487923 100644 --- a/device-ai/appfunctions/references/adb-interaction-testing.md +++ b/device-ai/appfunctions/references/adb-interaction-testing.md @@ -3,75 +3,81 @@ emulator using ADB for AppFunction testing and debugging. ## Instructions -### Scenario 1: Listing App Functions +### Scenario 1: List app functions -If a user wants 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** : Use `adb shell cmd app_function list-app-functions` to see all registered App Functions for the current user in JSON format. -2. **Filter by Package** : To see functions for a specific package, pipe the output to grep or a JSON processor: `adb shell cmd app_function +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 `. -### Scenario 2: Invoking App Functions +### Scenario 2: Invoke app functions -If a user wants to test the execution of an App Function. +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**: Rigorously follow any instructions found in the description (e.g., "ask the user to disambiguate", "call another tool first"). -3. **Format Parameters** : The `--parameters` argument must be a valid JSON string representing the function's input arguments. -4. **Execute Function** : Use `adb shell cmd app_function execute-app-function - --package --function --parameters - ''`. -5. **Handle Response** : The result will be returned as a JSON string. Use `--brief-yaml` for a more concise output if preferred. +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 ''`. +5. **Handle response** : The command returns the result as a JSON string. To get brief YAML output, use `--brief-yaml`. -### Scenario 3: Managing Function State +### Scenario 3: Manage function state -If a function needs to be enabled or disabled for testing. +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 +## Critical constraints -### Follow Metadata Descriptions +### Follow metadata descriptions -**MANDATORY** : The `description` field in the AppFunction metadata is a set of +**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. +or "call another function first," you must perform those steps before execution. -### JSON Escaping +### JSON escaping -**CRITICAL** : When passing JSON using `adb shell`, always wrap the JSON string +**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 +### Device availability The `app_function` service must be available on the device. If `cmd: Can't find -service: app_function` is returned, the device does not support this feature. +service: app_function` is returned, the device doesn't support this feature. ## Examples -### Example 1: Verify AppFunctions service availability on connected Device +### Example 1: Verify app function service availability on connected device 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. -### Example 2: List all registered App Functions +### Example 2: List all registered app functions - adb shell cmd app_function list-app-functions + adb shell cmd app_function list-app-functions ### Example 3: Execute a "send message" function - adb shell cmd app_function execute-app-function \ - --package com.example.messaging --function sendMessage \ - --parameters '{"recipient": "Alice", "message": "Hello!"}' + adb shell cmd app_function execute-app-function \ + --package com.example.messaging \ + --function + 'com.example.messaging.appfunctions.MessagingAppFunctionService#sendMessage' \ + --parameters '{"recipient": "Alice", "message": "Hello!"}' ### Example 4: Disable a specific function - adb shell cmd app_function set-enabled --package com.example.app \ - --function someFunction --state disable + adb shell cmd app_function set-enabled --package com.example.app \ + --function 'com.example.app.appfunctions.SomeAppFunctionService#someFunction' + --state disable ## Troubleshooting diff --git a/device-ai/appfunctions/references/feature-discovery-analysis.md b/device-ai/appfunctions/references/feature-discovery-analysis.md index b3d67b9..583f67c 100644 --- a/device-ai/appfunctions/references/feature-discovery-analysis.md +++ b/device-ai/appfunctions/references/feature-discovery-analysis.md @@ -2,22 +2,22 @@ Analyzes Android codebases to identify and recommend high-value AppFunctions. ## Instructions -### Workflow: Feature Discovery +### Discover features -1. **Analyze Manifest \& Entry Points** : Scan `AndroidManifest.xml` and Activity, Fragment, Service classes to identify core user journeys (e.g., Search, Create, Share). -2. **Identify Atomic Tasks**: Look for methods or logic that represent distinct, self-contained user outcomes. -3. **Evaluate AI Value**: Prioritize tasks that are frequently used or difficult to navigate using touch UI, but instead be expressed using voice or text (e.g., "Remind me to call Alice when I get home"). -4. **Recommend \& Justify**: List recommendations with a "Rationale" focusing on how an AI assistant adds value (efficiency, hands-free use, or multi-step automation). +1. **Analyze manifest and entry points** : Scan `AndroidManifest.xml` and Activity, Fragment, and Service classes to identify core user journeys, for example, search, create, or share. +2. **Identify atomic tasks**: Look for methods or logic that represent distinct, self-contained user outcomes. +3. **Evaluate AI value**: Prioritize tasks that are frequently used or difficult to navigate using touch UI, but instead be expressed using voice or text, for example "Remind me to call Alice when I get home." +4. **Recommend and justify**: List recommendations with a rationale. Focus on how an AI assistant adds value, such as efficiency, hands-free use, or multi-step automation. -## Critical Constraints +## Critical constraints -### Tool-First Thinking +### 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). -### Security \& Privacy +### Security and privacy Don't recommend exposing functions that handle raw credentials, financial secrets, or irreversible destructive actions without explicit user confirmation @@ -25,12 +25,47 @@ steps. ## Examples -### Example 1: Media App Discovery +### Example 1: Media app discovery -**Recommended AppFunction:** `playArtistRadio` +**Recommended AppFunction** : `playArtistRadio` -**Rationale:** Allows users to 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:** Artist Name (String). \ No newline at end of file +**Input required** : `artistName` as a `String`. + +### Example 2: Chat app contact discovery + +**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. + +**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. + +**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. + +**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 2f5bb2e..fb9292c 100644 --- a/device-ai/appfunctions/references/implementation-configuration.md +++ b/device-ai/appfunctions/references/implementation-configuration.md @@ -3,206 +3,271 @@ handling system-wide configuration, and managing build dependencies. ## Instructions -### Step 1: Gradle Dependencies \& KSP +### Step 1: Configure Gradle dependencies and KSP Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin Symbol Processing) plugin. -1. **Version Check**: Use the latest library versions from maven.google.com. +1. **Version check** : Use library version `1.0.0-alpha10` or later from maven.google.com. ```kotlin implementation(libs.androidx.appfunctions) -implementation(libs.androidx.appfunctions.service) ksp(libs.androidx.appfunctions.compiler) ```
- -```kotlin -ksp { - arg("appfunctions:aggregateAppFunctions", "true") -} -``` +

-### Step 2: App Metadata XML Setup +### Step 2: Set up app metadata XML Describe the app's capabilities to the LLM by defining `res/xml/app_metadata.xml`. - -Reference this in `AndroidManifest.xml` within the `` tag: +```xml + +``` - - - - - +
-### Step 3: Function Implementation +Register the service and reference the app metadata in `AndroidManifest.xml` +within the `` tag: + + +```xml + + + + + + + + +``` + +
+ +### Step 3: Implement functions When generating Kotlin code for AppFunctions, you MUST adhere to these rules: 1. **Annotations** : - Annotate the function with `@AppFunction(isDescribedByKDoc = true)`. - Annotate associated data classes with `@AppFunctionSerializable(isDescribedByKDoc = true)`. -2. **Parameter Strategy** : - - **First Parameter** : MUST be `androidx.appfunctions.AppFunctionContext`. - - **Specificity**: Keep parameters specific. State objects need to be unambiguous. - - **Optionality**: If a parameter is not essential, make it optional with a reasonable default value. -3. **Execution \& Threading** : +2. **Parameter strategy** : + - **Specificity**: Keep parameters specific. State objects must be unambiguous. + - **Optionality**: If a parameter isn't essential, make it optional with a default value. +3. **Execution and threading** : - Use `suspend` functions. - - Switch to the relevant Coroutine Dispatcher (e.g., `withContext(Dispatchers.IO)`) because AppFunction implementations run on the Android UI thread by default. -4. **Supported Types** : - - **Primitives** : `Int`, `Long`, `Float`, `Double`, `Boolean`. - - **Arrays** : `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`, `BooleanArray`. - - **Native Types** : `String`, `PendingIntent`, `Uri`, `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant`. (Prefer `LocalDateTime` or `Instant` for date/time). - - **Custom Objects** : Classes annotated with `@AppFunctionSerializable`. - - **Collections** : `List` of any supported non-primitive type. -5. **Default Values** : - - Use defaults that align with the type's "empty" state (e.g., `0` for `Int`, `null` for nullable, `emptyList()` for `List`). -6. **Error Handling** : + - To avoid blocking the Android UI thread, always run AppFunction implementations on a background dispatcher, such as `withContext(Dispatchers.IO)`. +4. **Supported types** : + - **Primitives** : `Int`, `Long`, `Float`, `Double`, `Boolean` + - **Arrays** : `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`, `BooleanArray` + - **Native types** : `String`, `PendingIntent`, `Uri`, `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant`. Prefer using `LocalDateTime` or `Instant` for date and time fields. + - **Custom objects** : Classes annotated with `@AppFunctionSerializable`. + - **Collections** : `List` of any supported non-primitive type +5. **Default values** : + - Use defaults that align with the type's empty state, such as `0` for `Int`, `null` for nullable objects, and `emptyList()` for `List`. +6. **Error handling** : - Throw subclasses of `androidx.appfunctions.AppFunctionException` to report errors to callers. 7. **Security** : - - Don't expose highly sensitive user data (passwords, financial details). + - Don't expose highly sensitive user data, such as passwords or financial details. - Don't expose irreversible destructive actions without confirmation steps. -### Step 4: (Optional) System Configuration for Dependency Injection +### Step 4: Set up dependency injection and service entry points -Only required for dependency injection configuration. Implement -`AppFunctionConfiguration.Provider` in the `Application` class to provide -instances of classes containing `@AppFunction` methods. +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. -**Example Hilt Integration:** +#### Recommended approach with Hilt + +Annotate your service with `@AndroidEntryPoint` and inject your data +repositories or use cases using standard `@Inject internal lateinit var`: ```kotlin -@HiltAndroidApp -class AppFunctionApplication : Application(), AppFunctionConfiguration.Provider { - @Inject lateinit var noteFunctions: NoteFunctions +@RequiresApi(36) +@AndroidEntryPoint +@AppFunctionServiceEntryPoint( + serviceName = "ConfigAppFunctionServiceHeader", + appFunctionXmlFileName = "config_app_function_service_header", +) +abstract class BaseAppFunctionServiceHeader : AppFunctionService() { + @Inject internal lateinit var messageRepository: MessageRepository - override val appFunctionConfiguration: AppFunctionConfiguration = - AppFunctionConfiguration.Builder() - .addEnclosingClassFactory(NoteFunctions::class.java) { noteFunctions } - .build() + @AppFunction(isDescribedByKDoc = true) + internal suspend fun send( + name: String, + endpointValue: String, + messageBody: String, + ): MessageResult { + return messageRepository.send(name, endpointValue, messageBody) + } } ```
-## Critical Constraints +#### Framework-agnostic approach with alternative dependency injection or service locators -### Parameter Ordering - -**CRITICAL** : The very first parameter of an `@AppFunction` method MUST be -`androidx.appfunctions.AppFunctionContext`. - -### KSP Compliance for Serializables - -**CRITICAL** : For `@AppFunctionSerializable` data classes, KSP **only** extracts -documentation if it is written as **inline KDoc** directly for each property -definition. NEVER use class-level `@param` or `@property` tags. - -### Package Integrity - -Configuration APIs and the `@AppFunction` annotation are located in -`androidx.appfunctions.service`. - -## Examples - -### Example: Serializable with Inline KDoc +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 -/** - * A note. - */ +@RequiresApi(36) +@AppFunctionServiceEntryPoint( + serviceName = "ServiceLocatorConfigAppFunctionService", + appFunctionXmlFileName = "service_locator_config_app_function_service", +) +abstract class BaseAppFunctionServiceLocator : AppFunctionService() { + // Example using a manual Service Locator / Application container + private val messageRepository by lazy { + (applicationContext as AppFunctionApplication).appContainer.messageRepository + } + // Or with Koin / alternative DI locators: + // private val messageRepository: MessageRepository by inject() + + @AppFunction(isDescribedByKDoc = true) + internal suspend fun send( + name: String, + endpointValue: String, + messageBody: String, + ): MessageResult { + 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. + +
+ +## Critical constraints + +### 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. + +### Package integrity + +Configuration APIs and the `@AppFunction` annotation are located in +`androidx.appfunctions`. + +## Examples + +### Example: Serializable with inline KDoc + + +```kotlin +/** The parameter to create the task. */ @AppFunctionSerializable(isDescribedByKDoc = true) -data class Note( - /** The note's identifier */ - val id: Int, - /** The note's title */ +data class CreateTaskParams( + /** The title of the task. */ + val title: String?, + /** The content of the task. */ + val content: String?, +) + +/** The user-created task. */ +@AppFunctionSerializable(isDescribedByKDoc = true) +data class Task( + /** The ID of the task. */ + val id: String, + /** The title of the task. */ val title: String, - /** The note's content */ - val content: String + /** The content of the task. */ + val content: String, ) ```
-### Example: Implementation Detail +
+ +### Example: Implementation detail ```kotlin -/** - * A note app's [AppFunction]s. - */ -class NoteFunctions @Inject constructor( - private val noteRepository: NoteRepository -) { - /** - * List all available notes in the app. - * - * @param appFunctionContext The execution context. - * @return A list of [Note] objects, or null if no notes exist. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun listNotes(appFunctionContext: AppFunctionContext): List? { - return noteRepository.appNotes.ifEmpty { null }?.toList() - } +@RequiresApi(36) +@AndroidEntryPoint +@AppFunctionServiceEntryPoint( + serviceName = "TaskAppFunctionService", + appFunctionXmlFileName = "task_app_function_service", +) +abstract class BaseTaskAppFunctionService : AppFunctionService() { + @Inject internal lateinit var taskRepository: TaskRepository /** - * Create a new note with a title and body content. + * Creates a task based on [createTaskParams]. * - * @param appFunctionContext The execution context. - * @param title The title of the note. - * @param content The body content of the note. - * @return The created [Note] object including its generated ID. + * @param createTaskParams The parameter to describe how to create the task. */ @AppFunction(isDescribedByKDoc = true) - suspend fun createNote( - appFunctionContext: AppFunctionContext, - title: String, - content: String - ): Note { - return noteRepository.createNote(title, content) + suspend fun createTask( + createTaskParams: CreateTaskParams, + ): Task = withContext(Dispatchers.IO) { + // Developers can use predefined exceptions to let the agent know + // why it failed. + if (createTaskParams.title == null && createTaskParams.content == null) { + throw AppFunctionInvalidArgumentException("Title or content should be non-null") + } + + val id = taskRepository.createTask( + createTaskParams.title, + createTaskParams.content + ) + + return@withContext taskRepository + .getTask(id) + ?.toTask() + ?: throw AppFunctionElementNotFoundException("Task not found for ID = $id") } - /** - * Update the title or content of an existing note. - * Required workflow: Call [listNotes] first to obtain valid note IDs. - * - * @param appFunctionContext The execution context. - * @param noteId The unique identifier of the note to edit. - * @param title The new title. If null, the existing title is preserved. - * @param content The new content. If null, the existing content is preserved. - * @return The updated [Note], or null if the [noteId] was not found. - */ - @AppFunction(isDescribedByKDoc = true) - suspend fun editNote( - appFunctionContext: AppFunctionContext, - noteId: Int, - title: String?, - content: String?, - ): Note? { - return noteRepository.updateNote(noteId, title, content) - } + // Maps internal TaskEntity + private fun TaskEntity.toTask() = Task(id = id, title = title, content = description) } ```
+
+ ## Troubleshooting ### Error: "AppFunction unavailable" or "Metadata missing" @@ -211,7 +276,7 @@ class NoteFunctions @Inject constructor( **Solution**: -1. Verify `@AppFunctionSerializable` classes use inline KDoc comments, NOT class-level `@param` tags. -2. Check that the `assets/app_function_v2.xml` file exists in the APK. +1. Verify `@AppFunctionSerializable` classes use inline KDoc comments, not class-level `@param` tags. +2. Check that the `assets/.xml` file exists in the APK. 3. Confirm the `ksp("androidx.appfunctions:appfunctions-compiler")` dependency is correctly applied. 4. Ensure the `ksp` argument `appfunctions:aggregateAppFunctions` is set to `"true"`. \ No newline at end of file diff --git a/device-ai/appfunctions/references/kdoc-refinement-optimization.md b/device-ai/appfunctions/references/kdoc-refinement-optimization.md index 90aa25b..c0a3209 100644 --- a/device-ai/appfunctions/references/kdoc-refinement-optimization.md +++ b/device-ai/appfunctions/references/kdoc-refinement-optimization.md @@ -2,51 +2,50 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol. ## Instructions -### Workflow: Agent-Centric Documentation +### Workflow: Agent-centric documentation -1. **Identify the Core Outcome** : Start the description with a strong, imperative verb (e.g., "Search", "Create", "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 +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"**. -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 declarations. KSP will **not** extract documentation from class-level tags. -4. **Error Surface Mapping** : Rewrite `@throws` descriptions to provide actionable recovery steps for the AI agent (e.g., "If "Error", suggest the user check their internet connection"). +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. +4. **Error surface mapping** : Rewrite `@throws` descriptions to provide useful recovery steps for the AI agent, for example "If "Error", suggest the user check their internet connection." -### Workflow: Global App Description (Server Instructions) +### Workflow: Global app description for server instructions When writing the `appfn:description` for `app_metadata.xml`, follow these instructions: -1. **Capture Cross-Function Relationships**: Explain dependencies or sequences between tools (e.g., "Always call 'authenticate' before fetching data"). -2. **Document Operational Patterns**: Guide the LLM on token conserving usage (e.g., "Use 'batch_update' over multiple 'update' calls"). -3. **Specify Constraints**: Define clear boundaries (e.g., "File operations limited to workspace", "Rate limit: 10 req/min"). -4. **Anti-Patterns** : - - DON'T repeat individual function descriptions. - - DON'T include marketing claims or subjective praise. - - DON'T attempt to prompt model personality or conversation style. +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." +3. **Specify constraints**: Define clear boundaries, for example, "File operations limited to workspace" or "Rate limit: 10 requests per minute." +4. **Anti-patterns** : + - Don't repeat individual function descriptions. + - Don't include marketing claims or subjective praise. + - Don't attempt to prompt model personality or conversation style. -## Critical Constraints +## Critical constraints -### Descriptive, Not Imperative +### 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...". -### No "Fluff" +### No "fluff" Remove conversational padding like "This method is used to..." or "Helpful for...". Be concise and technical. -### Inline KDoc for Serializables +### Inline KDoc for serializables -**MANDATORY** : For `@AppFunctionSerializable` classes, documentation MUST be -inline For each property. KSP ignores class-level `@param` or `@property` tags +**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be +inline for each property. KSP ignores class-level `@param` or `@property` tags for these classes. ## Examples -### Example: MCP Refactoring +### Example: MCP refactoring **Original**: @@ -54,24 +53,23 @@ for these classes. **Refined**: - /** - * Search for message recipients by name or email. - * Required workflow: Call this before "sendMessage" to obtain valid recipient IDs. - * @param query Search string for name/email. If null, returns 3 most recent contacts. - * @return List of "Recipient" objects matching the query. - */ + /** + * Search for message recipients by name or email. + * Required workflow: Call this before "sendMessage" to obtain valid recipient IDs. + * @param query Search string for name/email. If null, returns 3 most recent contacts. + * @return List of "Recipient" objects matching the query. + */ -### Example: Global App Description +### Example: Global app description **Refined**: This app provides functions for task management and team collaboration. Operational Patterns: - - Always use 'searchUsers' to resolve user handles to internal IDs before calling 'assignTask'. - - Prefer 'batchUpdateStatus' when modifying more than 3 tasks simultaneously to reduce - latency. + - Always use 'searchUsers' to resolve user handles to internal IDs before calling 'assignTask'. + - Prefer 'batchUpdateStatus' when modifying more than three tasks simultaneously to reduce latency. Constraints: - - Task titles are limited to 100 characters. - - Attachment uploads are limited to 5MB. \ No newline at end of file + - Task titles are limited to 100 characters. + - Attachment uploads are limited to 5 MB. \ No newline at end of file diff --git a/device-ai/appfunctions/references/migrate-to-service-entry-point.md b/device-ai/appfunctions/references/migrate-to-service-entry-point.md new file mode 100644 index 0000000..f350f61 --- /dev/null +++ b/device-ai/appfunctions/references/migrate-to-service-entry-point.md @@ -0,0 +1,226 @@ +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`. + +*** ** * ** *** + +## Architectural shift overview + +In lower versions of the AppFunctions API, for example version `1.0.0-alpha09`: + +- Applications require separate dependencies for core functionality, specifically `androidx.appfunctions:appfunctions`, and service components, specifically `androidx.appfunctions:appfunctions-service`. +- The application implements `AppFunctionConfiguration.Provider` on its `Application` class. +- You manually register enclosing class instantiation using `AppFunctionConfiguration.Builder().addEnclosingClassFactory(...)`. +- You place metadata property tags directly under ``. + +In version `1.0.0-alpha10` featuring `@AppFunctionServiceEntryPoint`: + +- The core and service dependencies are consolidated into a single runtime artifact, `androidx.appfunctions:appfunctions`, which eliminates the need for the standalone `appfunctions-service` library. +- A dedicated wrapper class extending `AppFunctionService` is annotated with `@AppFunctionServiceEntryPoint` and Hilt's `@AndroidEntryPoint` or an alternative dependency injection framework. +- The KSP compiler generates a concrete service subclass and an XML metadata schema file in `assets/`. +- The OS discovers and routes executions using a consolidated `` and `app_metadata` declaration in `AndroidManifest.xml`. + +### 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: + +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`. +3. **Annotation and context decoupling** : Move `@AppFunction` annotations to the new wrapper methods and drop `AppFunctionContext` parameters because `AppFunctionService` inherits directly from `Context`. +4. **Manifest registration** : Register the KSP-generated concrete service in `AndroidManifest.xml` with `BIND_APP_FUNCTION_SERVICE`, the `AppFunctionService` intent filter, and metadata property tags. + +*** ** * ** *** + +## Systematic migration steps + +### 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. + + // build.gradle.kts + dependencies { + implementation(libs.androidx.appfunctions) + - implementation(libs.androidx.appfunctions.service) + ksp(libs.androidx.appfunctions.compiler) + } + + # libs.versions.toml + [versions] + appfunctions = "1.0.0-alpha10" + + [libraries] + androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" } + - androidx-appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "appfunctions" } + androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctions" } + +> [!NOTE] +> **Note:** If another dependency in your project uses snapshot builds like `1.0.0-SNAPSHOT` or custom snapshot repositories from `https://androidx.dev/snapshots/...`, preserve your custom snapshot repository configuration in `settings.gradle.kts`. Otherwise, standard Google Maven repositories resolve `1.0.0-alpha10` directly. + +*** ** * ** *** + +### 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`. + +#### Recommended approach using Hilt + +Annotate your service with `@AndroidEntryPoint` and inject your data +repositories or use cases using standard `@Inject internal lateinit var`: + + +```kotlin +@RequiresApi(36) +@AndroidEntryPoint +@AppFunctionServiceEntryPoint( + serviceName = "MyAppFunctionService", + appFunctionXmlFileName = "my_app_function_service", // Do NOT include .xml extension +) +abstract class BaseAppFunctionService : AppFunctionService() { + @Inject internal lateinit var messageRepository: MessageRepository + + @AppFunction(isDescribedByKDoc = true) + internal suspend fun send( + name: String, + endpointValue: String, + messageBody: String, + ): MessageResult { + 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: + + +```kotlin +@RequiresApi(36) +@AppFunctionServiceEntryPoint( + serviceName = "ServiceLocatorMyAppFunctionService", + appFunctionXmlFileName = "service_locator_my_app_function_service", +) +abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() { + // Example using a manual Service Locator or Application container + private val messageRepository by lazy { + (applicationContext as AppFunctionApplication).appContainer.messageRepository + } + // Or with Koin / alternative DI locators: + // private val messageRepository: MessageRepository by inject() + + @AppFunction(isDescribedByKDoc = true) + internal suspend fun send( + name: String, + endpointValue: String, + messageBody: String, + ): MessageResult { + return messageRepository.send(name, endpointValue, messageBody) + } +} +``` + +
+ +> [!IMPORTANT] +> **Important:** The `appFunctionXmlFileName` parameter, for example `"my_app_function_service"`, mustn't include the `.xml` extension, as the KSP compiler automatically appends `.xml`. Passing `"my_app_function_service.xml"` results in the asset being named `"my_app_function_service.xml.xml"`. + +*** ** * ** *** + +### 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`. + + - suspend fun makeCall(appFunctionContext: AppFunctionContext, contactName: String?): PendingIntent + + suspend fun makeCall(contactName: String?): PendingIntent + +*** ** * ** *** + +### Remove legacy configuration provider + +Update your `Application` class by removing +`AppFunctionConfiguration.Provider` and its associated builder entry points: + + - abstract class BaseChatApplication : Application(), AppFunctionConfiguration.Provider { ... } + + abstract class BaseChatApplication : Application() + +*** ** * ** *** + +### 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. + +*** ** * ** *** + +### 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: + + +```xml + + + + + + + + +``` + +
+ +*** ** * ** *** + +## 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/`.* + +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/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md index 1938645..adb7fad 100644 --- a/devtools/android-cli/SKILL.md +++ b/devtools/android-cli/SKILL.md @@ -68,7 +68,7 @@ Capture an image of the current screen of a connected Android device and output ## Managing skills -Manage antigravity agent skills for Android using the `android skills` command. +Manage agent skills for Android using the `android skills` command. ## Inspecting UI Layouts diff --git a/devtools/android-cli/references/interact.md b/devtools/android-cli/references/interact.md index 099e66b..fa5eed6 100644 --- a/devtools/android-cli/references/interact.md +++ b/devtools/android-cli/references/interact.md @@ -74,7 +74,7 @@ To tap on this button, you would execute `adb shell input tap 152 23`. This taps "center": "[250,400]" } ``` -To scroll down on this list, you would execute `adb shell input swipe 250 400 600 500`. This swipes from the center to the bottom over 500ms. +To scroll down on this list, you would execute `adb shell input swipe 250 400 250 200 500`. This swipes from the center to the top over 500ms. # Android Interaction Rules 1. Always ensure text input fields have `"focused"` in their `"state"` list before entering text diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index 4e57942..4957098 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-07-09' + last-updated: '2026-07-18' keywords: - recipe - Android @@ -63,7 +63,7 @@ Code examples showcasing common patterns. ### Deep links -- *[Basic](https://developer.android.com/guide/navigation/navigation-3/recipes/deeplinks-basic)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. +- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. - *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior. ### Scenes 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 a7dfe7e..ba86f3c 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 @@ -438,12 +438,12 @@ For example: ScreenA(title = "Screen has ID: $id") } featureBSection() - < di>alogRouteD{ ScreenD() } + dialog{ ScreenD() } } fun NavGraphBuilder.featureBSection() { - BaseRouteB(startDestination = RouteB) { - ableRouteB { ScreenB() } + navigation(startDestination = RouteB) { + composable { ScreenB() } } } @@ -460,12 +460,12 @@ becomes: val entryProvider = entryProvider { entry{ key -> ScreenA(title = "Screen has ID: ${key.id}") } - featureBSection()< - e>ntryRouteD(metadata = DialogSceneStrategy.dialog()){ ScreenD() } + featureBSection() + entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } } - fun EntryProcopeNavKey.featureBSection() {< - e>ntryRouteB { ScreenB() } + fun EntryProviderScope.featureBSection() { + entry { ScreenB() } } ## Step 6: Replace `NavHost` with `NavDisplay` diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md new file mode 100644 index 0000000..66312de --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md @@ -0,0 +1,744 @@ +# Deep Link Basic Recipe + +This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. + +## How it works + +It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. + +## Demonstrated forms of deeplink + +The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: + +1. `HomeKey` - deeplink with an exact url (no deeplink arguments) +2. `UsersKey` - deeplink with path arguments +3. `SearchKey` - deeplink with query arguments + +See `MainActivity.deepLinkPatterns` for the actual url pattern of each. + +## Recipe structure + +This recipe consists of three main packages: + +1. `basic.deeplink` - Contains the two activities +2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc +3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks + +[![](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/basic) + +``` +package com.example.nav3recipes.deeplink.basic + +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS +import kotlinx.serialization.Serializable + +internal interface NavRecipeKey: NavKey { + val name: String +} + +@Serializable +internal object HomeKey: NavRecipeKey { + override val name: String = STRING_LITERAL_HOME +} + +@Serializable +internal data class UsersKey( + val filter: String, +): NavRecipeKey { + override val name: String = STRING_LITERAL_USERS + companion object { + const val FILTER_KEY = STRING_LITERAL_FILTER + const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" + const val FILTER_OPTION_ALL = "all" + } +} + +@Serializable +internal data class SearchKey( + val firstName: String? = null, + val ageMin: Int? = null, + val ageMax: Int? = null, + val location: String? = null, +): NavRecipeKey { + override val name: String = STRING_LITERAL_SEARCH +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.net.toUri +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FriendsList +import com.example.nav3recipes.common.deeplink.LIST_USERS +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT +import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult +import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher +import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern +import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest +import com.example.nav3recipes.deeplink.basic.util.KeyDecoder +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * Parses a target deeplink into a NavKey. There are several crucial steps involved: + * + * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable + * format (see [DeepLinkPattern]) + * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) + * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each + * other to facilitate comparison and finding a match + * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match + * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result + * to backstack key, regardless of what the conversion method may be. + * STEP 4. Associate the match results with the correct backstack key + * + * This recipes provides an example for each of the above steps by way of kotlinx.serialization. + * + * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional + * deeplink considerations are not included in this scope** + * - Create synthetic backStack + * - Multi-modular setup + * - DI + * - Managing TaskStack + * - Up button ves Back Button + * + */ +class MainActivity : ComponentActivity() { + /** STEP 1. Parse supported deeplinks */ + // internal so that landing activity can link to this in the kdocs + internal val deepLinkPatterns: List> = listOf( + // "https://www.nav3recipes.com/home" + DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), + // "https://www.nav3recipes.com/users/with/{filter}" + DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), + // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" + DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), + ) + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + // retrieve the target Uri + val uri: Uri? = intent.data + // associate the target with the correct backstack key + val key: NavKey = uri?.let { + /** STEP 2. Parse requested deeplink */ + val request = DeepLinkRequest(uri) + /** STEP 3. Compared requested with supported deeplink to find match*/ + val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> + DeepLinkMatcher(request, pattern).match() + } + /** STEP 4. If match is found, associate match to the correct key*/ + match?.let { + //leverage kotlinx.serialization's Decoder to decode + // match result into a backstack key + KeyDecoder(match.args) + .decodeSerializableValue(match.serializer) + } + } ?: HomeKey // fallback if intent.uri is null or match is not found + + /** + * Then pass starting key to backstack + */ + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { key -> + EntryScreen(key.name) { + TextContent("") + } + } + entry { key -> + EntryScreen("${key.name} : ${key.filter}") { + TextContent("") + val list = when { + key.filter.isEmpty() -> LIST_USERS + key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS + else -> LIST_USERS.take(5) + } + FriendsList(list) + } + } + entry { search -> + EntryScreen(search.name) { + TextContent("") + val matchingUsers = LIST_USERS.filter { user -> + (search.firstName == null || user.firstName == search.firstName) && + (search.location == null || user.location == search.location) && + (search.ageMin == null || user.age >= search.ageMin) && + (search.ageMax == null || user.age <= search.ageMax) + } + FriendsList(matchingUsers) + } + } + } + ) + } + } +} +``` + +``` +package com.example.nav3recipes.deeplink.basic + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.EMPTY +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE +import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY +import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM +import com.example.nav3recipes.common.deeplink.LOCATION_BC +import com.example.nav3recipes.common.deeplink.LOCATION_BR +import com.example.nav3recipes.common.deeplink.LOCATION_CA +import com.example.nav3recipes.common.deeplink.LOCATION_US +import com.example.nav3recipes.common.deeplink.MenuDropDown +import com.example.nav3recipes.common.deeplink.MenuTextInput +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE +import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE +import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH +import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * This activity allows the user to create a deep link and make a request with it. + * + * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct + * and trigger the deeplink request, and the [MainActivity] to show how an app can handle + * that request. + * + * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to + * demonstrate different types of supported deeplinks: + * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) + * 2. [UsersKey] - deeplink with path arguments + * 3. [SearchKey] - deeplink with query arguments + * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. + * + * **RECIPE STRUCTURE** This recipe consists of three main packages: + * 1. basic.deeplink - Contains the two activities + * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc + * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match + * the deeplinks + * + * See [MainActivity] for how the requested deeplink is handled. + */ +class CreateDeepLinkActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + /** + * UI for deeplink sandbox + */ + EntryScreen("Sandbox - Build Your Deeplink") { + TextContent("Base url:\n${PATH_BASE}/") + var showFilterOptions by remember { mutableStateOf(false) } + val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } + + var showQueryOptions by remember { mutableStateOf(false) } + var selectedFilter by remember { mutableStateOf("") } + val selectedSearchQuery = remember { mutableStateMapOf() } + + // manage path options + MenuDropDown( + menuOptions = MENU_OPTIONS_PATH, + ) { _, selection -> + selectedPath.value = selection + when (selection) { + PATH_SEARCH -> { + showQueryOptions = true + showFilterOptions = false + } + + PATH_INCLUDE -> { + showQueryOptions = false + showFilterOptions = true + } + + else -> { + showQueryOptions = false + showFilterOptions = false + } + } + } + + // manage path filter options, reset state if menu is closed + LaunchedEffect(showFilterOptions) { + selectedFilter = if (showFilterOptions) { + MENU_OPTIONS_FILTER.values.first().first() + } else { + "" + } + } + if (showFilterOptions) { + MenuDropDown( + menuOptions = MENU_OPTIONS_FILTER, + ) { _, selected -> + selectedFilter = selected + } + } + + // manage query options, reset state if menu is closed + LaunchedEffect(showQueryOptions) { + if (showQueryOptions) { + val initEntry = MENU_OPTIONS_SEARCH.entries.first() + selectedSearchQuery[initEntry.key] = initEntry.value.first() + } else { + selectedSearchQuery.clear() + } + } + if (showQueryOptions) { + MenuTextInput( + menuLabels = MENU_LABELS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + MenuDropDown( + menuOptions = MENU_OPTIONS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + } + + // form final deeplink url + val arguments = when (selectedPath.value) { + PATH_INCLUDE -> "/${selectedFilter}" + PATH_SEARCH -> { + buildString { + selectedSearchQuery.forEach { entry -> + if (entry.value.isNotEmpty()) { + val prefix = if (isEmpty()) "?" else "&" + append("$prefix${entry.key}=${entry.value}") + } + } + } + } + + else -> "" + } + val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" + TextContent("Final url:\n$finalUrl") + // deeplink to target + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent( + this@CreateDeepLinkActivity, + MainActivity::class.java + ) + // start activity with the url + intent.data = finalUrl.toUri() + startActivity(intent) + }) + } + } + } +} + +private const val KEY_PATH = "path" +private val MENU_OPTIONS_PATH = mapOf( + KEY_PATH to listOf( + STRING_LITERAL_HOME, + PATH_INCLUDE, + PATH_SEARCH, + ), +) + +private val MENU_OPTIONS_FILTER = mapOf( + UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), +) + +private val MENU_OPTIONS_SEARCH = mapOf( + SearchKey::firstName.name to listOf( + EMPTY, + FIRST_NAME_JOHN, + FIRST_NAME_TOM, + FIRST_NAME_MARY, + FIRST_NAME_JULIE + ), + SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) +) + +private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) + +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +/** + * Decodes the list of arguments into a a back stack key + * + * **IMPORTANT** This decoder assumes that all argument types are Primitives. + */ +@OptIn(ExperimentalSerializationApi::class) +internal class KeyDecoder( + private val arguments: Map, +) : AbstractDecoder() { + + override val serializersModule: SerializersModule = EmptySerializersModule() + private var elementIndex: Int = -1 + private var elementName: String = "" + + /** + * Decodes the index of the next element to be decoded. Index represents a position of the + * current element in the [descriptor] that can be found with [descriptor].getElementIndex. + * + * The returned index will trigger deserializer to call [decodeValue] on the argument at that + * index. + * + * The decoder continually calls this method to process the next available argument until this + * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more + * arguments to decode. + * + * This method should sequentially return the element index for every element that has its value + * available within [arguments]. + */ + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + var currentIndex = elementIndex + while (true) { + // proceed to next element + currentIndex++ + // if we have reached the end, let decoder know there are not more arguments to decode + if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE + val currentName = descriptor.getElementName(currentIndex) + // Check if bundle has argument value. If so, we tell decoder to process + // currentIndex. Otherwise, we skip this index and proceed to next index. + if (arguments.contains(currentName)) { + elementIndex = currentIndex + elementName = currentName + return elementIndex + } + } + } + + /** + * Returns argument value from the [arguments] for the argument at the index returned by + * [decodeElementIndex] + */ + override fun decodeValue(): Any { + val arg = arguments[elementName] + checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } + return arg + } + + override fun decodeNull(): Nothing? = null + + // we want to know if it is not null, so its !isNull + override fun decodeNotNullMark(): Boolean = arguments[elementName] != null +} +``` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri + +/** + * Parse the requested Uri and store it in a easily readable format + * + * @param uri the target deeplink uri to link to + */ +internal class DeepLinkRequest( + val uri: Uri +) { + /** + * A list of path segments + */ + val pathSegments: List = uri.pathSegments + + /** + * A map of query name to query value + */ + val queries = buildMap { + uri.queryParameterNames.forEach { argName -> + this[argName] = uri.getQueryParameter(argName)!! + } + } + + // TODO add parsing for other Uri components, i.e. fragments, mimeType, action +} +``` + +```` +package com.example.nav3recipes.deeplink.basic.util + +import android.net.Uri +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.encoding.CompositeDecoder +import java.io.Serializable + +/** + * Parse a supported deeplink and stores its metadata as a easily readable format + * + * The following notes applies specifically to this particular sample implementation: + * + * The supported deeplink is expected to be built from a serializable backstack key [T] that + * supports deeplink. This means that if this deeplink contains any arguments (path or query), + * the argument name must match any of [T] member field name. + * + * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] + * supports two deeplink patterns: + * ``` + * val deeplink1 = www.nav3recipes.com/home + * val deeplink2 = www.nav3recipes.com/profile/{userId} + * ``` + * Then two [DeepLinkPattern] should be created + * ``` + * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) + * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) + * ``` + * + * This implementation assumes a few things: + * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match + * 2. all query arguments are optional by way of nullable/has default value + * + * @param T the backstack key type that supports the deeplinking of [uriPattern] + * @param serializer the serializer of [T] + * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" + */ +internal class DeepLinkPattern( + val serializer: KSerializer, + val uriPattern: Uri +) { + /** + * Help differentiate if a path segment is an argument or a static value + */ + private val regexPatternFillIn = Regex("\\{(.+?)\\}") + + // TODO make these lazy + /** + * parse the path into a list of [PathSegment] + * + * order matters here - path segments need to match in value and order when matching + * requested deeplink to supported deeplink + */ + val pathSegments: List = buildList { + uriPattern.pathSegments.forEach { segment -> + // first, check if it is a path arg + var result = regexPatternFillIn.find(segment) + if (result != null) { + // if so, extract the path arg name (the string value within the curly braces) + val argName = result.groups[1]!!.value + // from [T], read the primitive type of this argument to get the correct type parser + val elementIndex = serializer.descriptor.getElementIndex(argName) + if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { + throw IllegalArgumentException( + "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." + ) + } + + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + // finally, add the arg name and its respective type parser to the map + add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) + } else { + // if its not a path arg, then its just a static string path segment + add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) + } + } + } + + /** + * Parse supported queries into a map of queryParameterNames to [TypeParser] + * + * This will be used later on to parse a provided query value into the correct KType + */ + val queryValueParsers: Map = buildMap { + uriPattern.queryParameterNames.forEach { paramName -> + val elementIndex = serializer.descriptor.getElementIndex(paramName) + // Ignore static query parameters that are not in the Serializable class + if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { + val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) + this[paramName] = getTypeParser(elementDescriptor.kind) + } + } + } + + /** + * Metadata about a supported path segment + */ + class PathSegment( + val stringValue: String, + val isParamArg: Boolean, + val typeParser: TypeParser + ) +} + +/** + * Parses a String into a Serializable Primitive + */ +private typealias TypeParser = (String) -> Serializable + +private fun getTypeParser(kind: SerialKind): TypeParser { + return when (kind) { + PrimitiveKind.STRING -> Any::toString + PrimitiveKind.INT -> String::toInt + PrimitiveKind.BOOLEAN -> String::toBoolean + PrimitiveKind.BYTE -> String::toByte + PrimitiveKind.CHAR -> String::toCharArray + PrimitiveKind.DOUBLE -> String::toDouble + PrimitiveKind.FLOAT -> String::toFloat + PrimitiveKind.LONG -> String::toLong + PrimitiveKind.SHORT -> String::toShort + else -> throw IllegalArgumentException( + "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." + ) + } +} +```` + +``` +package com.example.nav3recipes.deeplink.basic.util + +import android.util.Log +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.KSerializer + +internal class DeepLinkMatcher( + val request: DeepLinkRequest, + val deepLinkPattern: DeepLinkPattern +) { + /** + * Match a [DeepLinkRequest] to a [DeepLinkPattern]. + * + * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise + */ + fun match(): DeepLinkMatchResult? { + if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null + if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null + if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null + // exact match (url does not contain any arguments) + if (request.uri == deepLinkPattern.uriPattern) + return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) + + val args = mutableMapOf() + // match the path + request.pathSegments + .asSequence() + // zip to compare the two objects side by side, order matters here so we + // need to make sure the compared segments are at the same position within the url + .zip(deepLinkPattern.pathSegments.asSequence()) + .forEach { it -> + // retrieve the two path segments to compare + val requestedSegment = it.first + val candidateSegment = it.second + // if the potential match expects a path arg for this segment, try to parse the + // requested segment into the expected type + if (candidateSegment.isParamArg) { + val parsedValue = try { + candidateSegment.typeParser.invoke(requestedSegment) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) + return null + } + args[candidateSegment.stringValue] = parsedValue + } else if(requestedSegment != candidateSegment.stringValue){ + // if it's path arg is not the expected type, its not a match + return null + } + } + // match queries (if any) + request.queries.forEach { query -> + val name = query.key + // If the pattern does not define this query parameter, ignore it. + // This prevents a NullPointerException. + val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach + + val queryParsedValue = try { + queryStringParser.invoke(query.value) + } catch (e: IllegalArgumentException) { + Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) + return null + } + args[name] = queryParsedValue + } + // provide the serializer of the matching key and map of arg names to parsed arg values + return DeepLinkMatchResult(deepLinkPattern.serializer, args) + } +} + + +/** + * Created when a requested deeplink matches with a supported deeplink + * + * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink + * @param serializer serializer for [T] + * @param args The map of argument name to argument value. The value is expected to have already + * been parsed from the raw url string back into its proper KType as declared in [T]. + * Includes arguments for all parts of the uri - path, query, etc. + * */ +internal data class DeepLinkMatchResult( + val serializer: KSerializer, + val args: Map +) + +const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" +``` + +``` +package com.example.nav3recipes.deeplink.basic.ui + +import com.example.nav3recipes.deeplink.basic.SearchKey + +/** + * String resources + */ +internal const val STRING_LITERAL_FILTER = "filter" +internal const val STRING_LITERAL_HOME = "home" +internal const val STRING_LITERAL_USERS = "users" +internal const val STRING_LITERAL_SEARCH = "search" +internal const val STRING_LITERAL_INCLUDE = "include" +internal const val PATH_BASE = "https://www.nav3recipes.com" +internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" +internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" +internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" + +internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" +internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + + "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + + "&${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/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 7ad9e5e..2812549 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-06-09' + last-updated: '2026-07-18' keywords: - R8 - proguard 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 69c4295..a80f19d 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 @@ -47,62 +47,97 @@ resource shrinking](https://developer.android.com/topic/performance/app-optimiza ## Enable optimization -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 as shown in the following code. We recommend -that you always enable both settings. We also 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 + +To enable app optimization for AGP 9.3 and higher: + +- Set `enable = true` in the `optimization` block in your release build's app-level build script. +- 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). ### Kotlin -```kotlin -android { - buildTypes { - release { - - // Enables code-related app optimization. - isMinifyEnabled = true - - // Enables resource shrinking. - isShrinkResources = true - - proguardFiles( - // Default file with automatically generated optimization rules. - getDefaultProguardFile("proguard-android-optimize.txt"), - - ... - ) - ... + android { + buildTypes { + release { + optimization { + enable = true // Enables code and resource optimizations. + } + } } } - ... -} -``` ### Groovy -```groovy -android { - buildTypes { - release { - - // Enables code-related app optimization. - minifyEnabled = true - - // Enables resource shrinking. - shrinkResources = true - - // Default file with automatically generated optimization rules. - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') - - ... + android { + buildTypes { + release { + optimization { + enable = true // Enables code and resource optimizations. + } + } + } + } + +### 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. + +### Legacy DSL (Kotlin) + + android { + buildTypes { + release { + + // Enables code optimizations. + isMinifyEnabled = true + + // Enables resource shrinking. + isShrinkResources = true + + proguardFiles( + // Default file with automatically generated optimization rules. + getDefaultProguardFile("proguard-android-optimize.txt"), + + ... + ) + ... + } + } + ... + } + +### Legacy DSL (Groovy) + + android { + buildTypes { + release { + + // Enables code optimizations. + minifyEnabled = true + + // Enables resource shrinking. + shrinkResources = true + + // Default file with automatically generated optimization rules. + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') + + ... + } } } -} -``` ## Improve R8 optimization @@ -128,10 +163,8 @@ R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https ## Optimize resource shrinking for even smaller apps -The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource -shrinking, which aims to integrate resource and code optimization to create even -smaller and faster 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 @@ -147,8 +180,6 @@ removed. 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: - 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 @@ -159,7 +190,7 @@ configuration. 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: - android.enableR8.fullMode=false # Remove this line from your codebase. +`bash android.enableR8.fullMode=false # Remove this line from your codebase.` Note that enabling app optimization makes stack traces difficult to understand, especially if R8 renames class or method names. To get stack traces that @@ -190,6 +221,7 @@ 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.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). | diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 9927e22..0493cd7 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-07-09' + last-updated: '2026-07-18' keywords: - android - engage @@ -23,48 +23,48 @@ required Engage entities for each vertical. Follow these steps to assist the developer: -1. **Identify Vertical and Cluster:** +1. **Identify vertical and cluster:** - Ask the developer which vertical their app belongs to based on **[references/schemas/](references/schemas)**. - - Check if the integration is for TV or Mobile. Read the TV-specific sections in [patterns.md](references/patterns.md) as well if the integration is for TV. - - Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. (e.g., `com.google.android.engage.food.service.AppEngageFoodClient`). - - **Note:** Initializing the client class requires a `Context` parameter (e.g., `AppEngageFoodClient(context)`). + - Check if the integration is for TV or mobile. If the integration is for TV, read the TV-specific sections in [patterns.md](references/patterns.md) as well. + - Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. For example, `com.google.android.engage.food.service.AppEngageFoodClient`. + - **Note:** Initializing the client class requires a `Context` parameter. For example, `AppEngageFoodClient(context)`. - Always refer to [common.md](references/common.md) for common entities. - Ask which cluster type they want to publish from the supported cluster types for that vertical. - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. -2. **Generate Structured Boilerplate Code:** +2. **Generate structured boilerplate code:** - Create a new directory for all Engage-related code. Name the directory to match the naming convention of the existing codebase. - Generate the following classes using templates in [patterns.md](references/patterns.md): - - `Constants`: Holds constant values like attempt counts, publish types. - - `ItemToEntityConverter`: Converts app's local models to Engage's Entity models. - - `ClusterRequestFactory`: Constructs the publish requests. - - `EngageWorker`: Handles the actual publishing and publish errors using WorkManager. - - `EngagePublisher`: Orchestrates periodic and one-time jobs. - - `EngageBroadcastReceiver`: Listens for AppEngageService intents and starts a one-time publish job from `EngagePublisher`. **Important** : Implement both **static registration** and **dynamic registration** patterns, including the companion object `register` method inside the `EngageBroadcastReceiver` class. -3. **Suggest Entity Mapping:** + - `Constants`: holds constant values such as attempt counts and publish types. + - `ItemToEntityConverter`: converts the app's local models to Engage's Entity models. + - `ClusterRequestFactory`: constructs the publish requests. + - `EngageWorker`: handles the actual publishing and publish errors using WorkManager. + - `EngagePublisher`: orchestrates periodic and one-time jobs. + - `EngageBroadcastReceiver`: listens for AppEngageService intents and starts a one-time publish job from `EngagePublisher`. **Important** : Implement both **static registration** and **dynamic registration** patterns, including the companion object `register` method inside the `EngageBroadcastReceiver` class. +3. **Suggest entity mapping:** - - Ask the developer to provide their local model schema(e.g., a data class or a JSON snippet). + - Ask the developer to provide their local model schema (for example, a data class or a JSON snippet). - If they haven't provided one, share entities from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory as a guide. - Once the local model is identified, suggest a mapping to the corresponding Engage entity. - - Generate the conversion logic using the `ItemToEntityConverter` pattern in [patterns.md](references/patterns.md) and add it to the generated `{ENGAGE_CODE_DIR}/ItemToEntityConverter` -4. **Suggest Data Source:** + - Generate the conversion logic using the `ItemToEntityConverter` pattern in [patterns.md](references/patterns.md) and add it to the generated `{ENGAGE_CODE_DIR}/ItemToEntityConverter`. +4. **Suggest data source:** - Ask the developer to provide the source of actual data you'll publish. - - Once the source of data is identified, use the source of data to fetch data in app's local model schema. - - Use `{ENGAGE_CODE_DIR}/ItemToEntityConverter` to convert this data to Engage entity. + - 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. - 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:** +5. **Gradle and manifest updates:** - Suggest updates to `build.gradle` and `AndroidManifest.xml`. - For mobile apps, use [patterns.md](references/patterns.md). - For TV apps, use the TV-specific sections in [patterns.md](references/patterns.md). - 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: There's no separate import according to vertical except TV. For each vertical other than TV 'com.google.android.engage:engage-core:1.5.12' is enough. + - 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:** - Perform a Gradle sync. @@ -72,51 +72,45 @@ Follow these steps to assist the developer: - 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: - \[ \] Verify that all the engage related files are created in - `{ENGAGE_CODE_DIR}/`: - - `Constants` - - `ItemToEntityConverter` - - `ClusterRequestFactory` - - `EngageWorker` - - `{cluster_type}Publisher` - - `EngageBroadcastReceiver` - \[ \] Verify that app's local model is converted to Engage entity by populating - the fields correctly in the model in `{ENGAGE_CODE_DIR}/ - ItemToEntityConverter`. - \[ \] Verify that `{ENGAGE_CODE_DIR}/EngageWorker` uses the data source - identified in Step 4. - \[ \] Verify that `EngageBroadcastReceiver.register(context)` is called within - the `Application` class or `MainActivity` to register the receiver - dynamically. - \[ \] Verify that `AndroidManifest.xml` contains the static `` - declaration for `EngageBroadcastReceiver` with the necessary intent actions. +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` + - `ItemToEntityConverter` + - `ClusterRequestFactory` + - `EngageWorker` + - `{cluster_type}Publisher` + - `EngageBroadcastReceiver` + - \[ \] Verify that app's local model is converted to Engage entity by populating the fields correctly in the model in `{ENGAGE_CODE_DIR}/ItemToEntityConverter`. + - \[ \] Verify that all image URIs in `ItemToEntityConverter` point to images matching the strict aspect ratio requirements of the vertical (for example, 16:9, 1:1, 2:3). + - \[ \] Verify that `{ENGAGE_CODE_DIR}/EngageWorker` uses the data source identified in Step 4. + - \[ \] Verify that `EngageBroadcastReceiver.register(context)` is called within the `Application` class or `MainActivity` to register the receiver dynamically. + - \[ \] Verify that `AndroidManifest.xml` contains the static `` declaration for `EngageBroadcastReceiver` with the necessary intent actions. - **Important** : Explicitly instruct the developer to call `EngageBroadcastReceiver.register(context)` inside their custom `Application` class `onCreate()` (or their main activity `onCreate()`) to dynamically register the receiver. Stress that **both** static and dynamic registrations are required for the integration to function. -## Reference Materials +## Reference materials - **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:** +- **Vertical-specific guides:** - [Food Vertical](references/android/guide/playcore/engage/food.md) - [Watch Vertical](references/android/guide/playcore/engage/watch.md) - [Listen Vertical](references/android/guide/playcore/engage/listen.md) - [Read Vertical](references/android/guide/playcore/engage/read.md) - [Shopping Vertical](references/android/guide/playcore/engage/shopping.md) - - [Social Vertical](references/android/guide/playcore/engage/social.md) + - [Social Vertical](https://developer.android.com/guide/playcore/engage/social) - [Travel Vertical](references/android/guide/playcore/engage/travel.md) - - [Health \& Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) + - [Health and Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) - [Other Verticals](references/android/guide/playcore/engage/otherverticals.md) - [TV Getting Started](references/android/guide/playcore/engage/tv/getting-started.md) - [TV Recommendations](references/android/guide/playcore/engage/tv/recommendations.md) - [TV Continue Watching](references/android/guide/playcore/engage/tv/continue-watching/index.md) - [TV Entitlements](references/android/guide/playcore/engage/tv/entitlements.md) -- **Vertical-Specific Schemas:** +- **Vertical-specific schemas:** - [Food Schema](references/schemas/food.md) - [Watch Schema](references/schemas/watch.md) 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 deleted file mode 100644 index b1e8f78..0000000 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md +++ /dev/null @@ -1,702 +0,0 @@ -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. - -## Integration detail - -The following section captures the integration detail. - -### Terminology - -***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. - -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. - -- Post - - - Image in portrait mode and Timestamp, or - - Image in portrait mode + text content and Timestamp -- Profile - - - Avatar, Name or Handle, Additional image -- Interactions - - - Count and label only, or - - Count and visual (icon) - -**SocialPostEntity** contains profile, post and interaction related metadata. - -- Profile - - - Avatar, Name or Handle, additional text, additional image -- Post - - - Text and Timestamp, or - - Rich media (image or rich URL) and Timestamp, or - - Text and rich media (image or rich URL) and Timestamp, or - - Video preview (thumbnail and duration) and Timestamp -- Interactions - - - Count \& label only, or - - Count \& visual (icon) - -### Pre-work - -Minimum API level: 19 - -Add the `com.google.android.engage:engage-core` library to your app: - - dependencies { - // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.6.0' - } - -### Summary - -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: - -| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | -|---|---|---|---| -| Recommendation Cluster(s) | At most 7 | At least 1 (`PortraitMediaEntity`, or `SocialPostEntity`) | At most 50 (`PortraitMediaEntity`, or `SocialPostEntity`) | - -### 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: - -1. `PortraitMediaEntity` -2. `SocialPostEntity` - -The charts below outline available attributes and requirements for each type. - -#### `PortraitMediaEntity` - -| Attribute | Requirement | Description | Format | -|---|---|---|---| -| Action URI | **Required** for all surfaces other than Google TV | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | -| PlatformSpecificPlayback | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | -| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | -| Comments Summary | Optional | Summary of comments for the post. | String | -| **Post related metadata (Required)** |||| -| Image(s) | Required | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | -| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | -| Is video content | Optional | Is the post a video? | boolean | -| Video duration | Optional | The duration of the video in milliseconds. | Long | -| **Profile related metadata (Optional)** |||| -| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel" | String(recommended max 25 chars) | -| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Additional Image | Optional | Profile badge. for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Interactions related metadata (Optional)** |||| -| Count | Optional | Indicate the number of interactions, for example - "3.7 M.". **Note:** If both Count and Count Value are provided, Count will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| CountWithOptionalLabel | Optional | Indicate the number of interactions with an optional label, for example - "3.7 M Likes.". **Note:** If both CountWithOptionalLabel and Count Value are provided, one of them will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| Count Value | Optional | The number of interactions as a value. **Note:** Provide Count Value instead of Count if your app doesn't handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count is used. | Long | -| Label | Optional | Indicate what the interaction label is for. For example - "Likes". | String | -| Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| -| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | -| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | - -#### `SocialPostEntity` - -| Attribute | Requirement | Description | Format | -|---|---|---|---| -| Action URI | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | -| PlatformSpecificPlayback URIs | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | -| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | -| Comments Summary | Optional | Summary of comments for the post. | String | -| **Post related metadata (Required)** At least one of TextContent, Image or WebContent is required |||| -| Image(s) | Optional | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | -| **Video Content (Optional)** |||| -| Duration | Required | The duration of the video in milliseconds. | Long | -| Image | Required | Preview image of the video content. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Link Preview (Optional)** |||| -| Link Preview - Title | Required | Text to indicate the title of the web page content | String | -| Link Preview - Hostname | Required | Text to indicate the web page owner, eg "INSIDER" | String | -| Link Preview - Image | Optional | Hero image for the web content | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | -| **Profile related metadata (Optional)** |||| -| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel." | String(recommended max 25 chars) | -| Additional Text | Optional | Could be used as profile id or handle or additional metadata For example "@John-Doe", "5M followers", "You might like", "Trending", "5 new posts" | String(recommended max 40 chars) | -| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Additional Image | Optional | Profile badge, for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Interactions related metadata (Optional)** |||| -| Count | Required | Indicate the number of interactions, for example - "3.7 M." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| CountWithOptionalLabel | Required | Indicate the number of interactions with an optional label, for example - "3.7 M Likes." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| Label | Optional If not provided, **Visual** must be provided. | Indicate what the interaction is for. For example - "Likes." | String (recommended max 20 chars for count + label combined) | -| Visual | Optional If not provided, **Label** must be provided. | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| -| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | -| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | - -#### Image specifications - -The images are required to be hosted on public CDNs so that Google can access -them. - -*File formats* - -PNG, JPG, static GIF, WebP - -*Maximum file size* - -5120 KB - -*Additional recommendations* - -- **Image safe area:** Put your important content in the center 80% of the image. -- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. - -### 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) - -`AppEngageSocialClient` is responsible for publishing social clusters. - -There are following APIs to publish clusters in the client: - -- `isServiceAvailable` -- `publishRecommendationClusters` -- `publishUserAccountManagementRequest` -- `updatePublishStatus` -- `deleteRecommendationsClusters` -- `deleteUserManagementCluster` -- `deleteClusters` - -#### `isServiceAvailable` - -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) - -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 | -| Featured Cluster | `TYPE_FEATURED` | 2 | -| Continuation Cluster | `TYPE_CONTINUATION` | 3 | -| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | -| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | - -### Kotlin - - 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 { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - } - -### Java - - ServiceAvailabilityRequest request = - new ServiceAvailabilityRequest.Builder() - .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) - .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) - .build(); - - client.isServiceAvailable(request).addOnCompleteListener(task -> { - if (task.isSuccessful()) { - Map availabilityMap = task.getResult(); - if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { - // Proceed with publishing continuation content - } - if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { - // Proceed with publishing recommendation content - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - }); - -###### 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. - -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) - -### Kotlin - - client.isServiceAvailable.addOnCompleteListener { task -> - if (task.isSuccessful) { - // Handle IPC call success - if(task.result) { - // Service is available on the device, proceed with content publish - // calls. - } else { - // Service is not available, no further action is needed. - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - } - -### Java - - client.isServiceAvailable().addOnCompleteListener(task - > { - if (task.isSuccessful()) { - // Handle success - if(task.getResult()) { - // Service is available on the device, proceed with content publish - // calls. - } else { - // Service is not available, no further action is needed. - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - }); - -> [!NOTE] -> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. - -#### `publishRecommendationClusters` - -This API is used to publish a list `RecommendationCluster` objects. - -A `RecommendationCluster` object can have the following attributes: - -| Attribute | Requirement | Description | -|---|---|---| -| List of SocialPostEntity, or PortraitMediaEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | -| Title | **Required** | The title for the Recommendation Cluster (for example, *Latest from your friends*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | -| Subtitle | Optional | The subtitle for the Recommendation Cluster. | -| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | - -> [!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. - -> [!IMPORTANT] -> **Important:** For social apps, it's critical to update recommendations 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. - -### Kotlin - - client.publishRecommendationClusters( - PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster( - RecommendationCluster.Builder() - .addEntity(entity1) - .addEntity(entity2) - .setTitle("Latest from your friends") - .build()) - .build()) - -### Java - - client.publishRecommendationClusters( - new PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster( - new RecommendationCluster.Builder() - .addEntity(entity1) - .addEntity(entity2) - .setTitle("Latest from your friends") - .build()) - .build()); - -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. - -#### `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) - -The following metadata is part of the Sign In Card - - -| Attribute | Requirement | Description | -|---|---|---| -| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | -| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | -| Title | Optional - If not provided, Image must be provided | Title on the Card | -| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | -| Subtitle | Optional | Optional Subtitle on the Card | - -> [!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. - -### Kotlin - - var SIGN_IN_CARD_ENTITY = - SignInCardEntity.Builder() - .addPosterImage( - Image.Builder() - .setImageUri(Uri.parse("http://www.x.com/image.png")) - .setImageHeightInPixel(500) - .setImageWidthInPixel(500) - .build()) - .setActionText("Sign In") - .setActionUri(Uri.parse("http://xx.com/signin")) - .build() - - client.publishUserAccountManagementRequest( - PublishUserAccountManagementRequest.Builder() - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) - .build()); - -### Java - - SignInCardEntity SIGN_IN_CARD_ENTITY = - new SignInCardEntity.Builder() - .addPosterImage( - new Image.Builder() - .setImageUri(Uri.parse("http://www.x.com/image.png")) - .setImageHeightInPixel(500) - .setImageWidthInPixel(500) - .build()) - .setActionText("Sign In") - .setActionUri(Uri.parse("http://xx.com/signin")) - .build(); - - client.publishUserAccountManagementRequest( - new PublishUserAccountManagementRequest.Builder() - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) - .build()); - -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. - -#### `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 : - -- 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. -- It helps developers provide insights into when the data is published versus not. -- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. - -The list of eligible publish status codes are : - - // Content is published - AppEngagePublishStatusCode.PUBLISHED, - - // Content is not published as user is not signed in - AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, - - // Content is not published as user is not subscribed - AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, - - // Content is not published as user location is ineligible - AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, - - // Content is not published as there is no eligible content - AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, - - // Content is not published as the feature is disabled by the client - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, - - // Content is not published as the feature due to a client error - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, - - // Content is not published as the feature due to a service error - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, - - // Content is not published due to some other reason - // 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** - -### Kotlin - - client.updatePublishStatus( - PublishStatusRequest.Builder() - .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) - .build()) - -### Java - - client.updatePublishStatus( - new PublishStatusRequest.Builder() - .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) - .build()); - -#### `deleteRecommendationClusters` - -This API is used to delete the content of Recommendation Clusters. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteRecommendationClusters() - -### Java - - 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. - -#### `deleteUserManagementCluster` - -This API is used to delete the content of UserAccountManagement Cluster. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteUserManagementCluster() - -### Java - - 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. - -#### `deleteClusters` - -This API is used to delete the content of a given cluster type. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteClusters( - DeleteClustersRequest.Builder() - .addClusterType(ClusterType.TYPE_RECOMMENDATION) - ... - .build()) - -### Java - - client.deleteClusters( - new DeleteClustersRequest.Builder() - .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. - -#### 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. - - client.publishRecommendationClusters( - new PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster(...) - .build()) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - // do something - } else { - Exception exception = task.getException(); - if (exception instanceof AppEngageException) { - @AppEngageErrorCode - int errorCode = ((AppEngageException) exception).getErrorCode(); - if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { - // do something - } - } - } - }); - -The error is returned as an `AppEngageException` with the cause included as an -error code. - -| Error code | Error name | Note | -|---|---|---| -| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | -| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | -| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | -| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | -| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | -| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | -| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | - -### 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. - -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. - -### Kotlin - - class AppEngageBroadcastReceiver : BroadcastReceiver(){ - // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION - // broadcast is received - } - - fun registerBroadcastReceivers(context: Context){ - var context = context - context = context.applicationContext - - // Register Recommendation Cluster Publish Intent - context.registerReceiver(AppEngageBroadcastReceiver(), - IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), - com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, - /*scheduler=*/null) - } - -### Java - - class AppEngageBroadcastReceiver extends BroadcastReceiver { - // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast - // is received - } - - public static void registerBroadcastReceivers(Context context) { - - context = context.getApplicationContext(); - - // Register Recommendation Cluster Publish Intent - context.registerReceiver(new AppEngageBroadcastReceiver(), - new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), - com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, - /*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. - - - - - - - - - -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). - -## 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. - -## Next steps - -After completing this integration, your next steps are as follows: - -- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. -- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. -- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. -- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation**, clusters will be published and visible to users. \ No newline at end of file diff --git a/play/engage-sdk-integration/references/clusters.md b/play/engage-sdk-integration/references/clusters.md index 6791095..efe5025 100644 --- a/play/engage-sdk-integration/references/clusters.md +++ b/play/engage-sdk-integration/references/clusters.md @@ -1,4 +1,4 @@ -This file defines the structure of various clusters in the Engage SDK. +The Engage SDK defines cluster structures as shown in this reference: { "clusters": { @@ -107,7 +107,7 @@ This file defines the structure of various clusters in the Engage SDK. }, "recommendationClusterType": { "type": "@RecommendationClusterType int", - "requirement": "Optional", + "requirement": "Required", "setter": "setRecommendationClusterType(@RecommendationClusterType int)", "getter": "getRecommendationClusterType()" } diff --git a/play/engage-sdk-integration/references/patterns.md b/play/engage-sdk-integration/references/patterns.md index be290a9..099c410 100644 --- a/play/engage-sdk-integration/references/patterns.md +++ b/play/engage-sdk-integration/references/patterns.md @@ -2,10 +2,10 @@ Setting up the `BroadcastReceiver` correctly requires **both** static and dynamic registration. Static registration allows the app to receive broadcasts -even when it is not running, while dynamic registration is required on newer +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 +### BroadcastReceiver implementation ```kotlin @@ -69,13 +69,13 @@ class EngageBroadcastReceiver : BroadcastReceiver() {
-### Static Registration (AndroidManifest.xml) +### Static registration in AndroidManifest.xml Add the `` tag inside the `` block in `AndroidManifest.xml` -```kotlin +```xml ClusterType.TYPE_RECOMMENDATION + Constants.PUBLISH_TYPE_FEATURED -> ClusterType.TYPE_FEATURED + Constants.PUBLISH_TYPE_CONTINUATION -> ClusterType.TYPE_CONTINUATION + Constants.PUBLISH_TYPE_USER_ACCOUNT_MANAGEMENT -> ClusterType.TYPE_ENGAGEMENT + else -> ClusterType.TYPE_UNKNOWN + } + + if (intendedClusterType != ClusterType.TYPE_UNKNOWN) { + val request = ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(intendedClusterType) + .build() + val availabilityMap = client.isServiceAvailable(request).await() + if (availabilityMap[intendedClusterType] != true) { + return Result.failure() + } } - val publishType = inputData.getString(Constants.PUBLISH_TYPE_KEY) return when (publishType) { Constants.PUBLISH_TYPE_RECOMMENDATIONS -> publishRecommendations() // Constants.PUBLISH_TYPE_FEATURED -> publishFeatured() @@ -411,16 +423,22 @@ object ItemToEntityConverter {
-## Dependency Specifications (libs.versions.toml) +> **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. -This skill specifies all dependencies following in `libs.versions.toml` format. +## 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. [versions] - engage-core = "1.5.12" - engage-tv = "1.0.6" + engage-core = "1.6.0" + engage-tv = "1.1.0" playServicesOssLicenses = "17.5.1" workManager = "2.11.2" coroutines = "1.10.2" @@ -435,22 +453,47 @@ project. kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } -## TV Integrations +## 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. + + +```kotlin +fun convertMovie(movie: MovieData): MovieEntity { + val ratingSystem = RatingSystem.Builder() + .setAgencyName("MPAA") + .setRating("PG-13") + .build() + return MovieEntity.Builder() + .setEntityId(movie.id) + .setName(movie.title) + // ... other fields + .addContentRating(ratingSystem) // Recommended API + .addContentRatingsLegacy(listOf("MPAA:PG-13")) // Legacy API for backward compatibility + .build() +} +``` + +
+ +## TV integrations The following patterns and configurations are specific to Android TV integrations. -### AndroidManifest.xml (TV) +### AndroidManifest.xml for TV -```kotlin +```xml ```
-### PlatformSpecificUri Example +### PlatformSpecificUri example ```kotlin @@ -468,7 +511,7 @@ val platformSpecificPlaybackUris = listOf(
-### AccountProfile Example +### AccountProfile example ```kotlin diff --git a/play/engage-sdk-integration/references/requests.md b/play/engage-sdk-integration/references/requests.md index c65f0a6..00849e6 100644 --- a/play/engage-sdk-integration/references/requests.md +++ b/play/engage-sdk-integration/references/requests.md @@ -1,4 +1,4 @@ -This file defines the request structures for publishing various data models in +Defines the request structures for publishing various data models in the Engage SDK. { @@ -15,7 +15,8 @@ the Engage SDK. "type": "@NonNull AccountProfile", "requirement": "Optional", "setter": "setAccountProfile(@NonNull AccountProfile)", - "getter": "getAccountProfile()" + "getter": "getAccountProfile()", + "description": "Required for personalization and cross-device syncing of recommendations." }, "syncAcrossDevices": { "type": "Boolean", @@ -227,5 +228,17 @@ the Engage SDK. "getter": "getReservationCluster()" } } + }, + "ServiceAvailabilityRequest": { + "package": "com.google.android.engage.service.ServiceAvailabilityRequest", + "fields": { + "intendedClusterTypes": { + "type": "List", + "requirement": "Required", + "adder": "addIntendedClusterType(@ClusterType int)", + "adderAll": "addAllIntendedClusterTypes(List)", + "getter": "getIntendedClusterTypes()" + } + } } } \ No newline at end of file diff --git a/play/engage-sdk-integration/references/schemas/food.md b/play/engage-sdk-integration/references/schemas/food.md index 8c8e6fa..5299824 100644 --- a/play/engage-sdk-integration/references/schemas/food.md +++ b/play/engage-sdk-integration/references/schemas/food.md @@ -319,7 +319,7 @@ This file defines the schema for the FOOD vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishFoodShoppingCarts": "PublishFoodShoppingCartsRequest", diff --git a/play/engage-sdk-integration/references/schemas/listen.md b/play/engage-sdk-integration/references/schemas/listen.md index 96c6e91..56b04fe 100644 --- a/play/engage-sdk-integration/references/schemas/listen.md +++ b/play/engage-sdk-integration/references/schemas/listen.md @@ -825,7 +825,7 @@ This file defines the schema for the LISTEN vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishContinuationCluster": "PublishContinuationClusterRequest", diff --git a/play/engage-sdk-integration/references/schemas/other.md b/play/engage-sdk-integration/references/schemas/other.md index a12b69d..b55307c 100644 --- a/play/engage-sdk-integration/references/schemas/other.md +++ b/play/engage-sdk-integration/references/schemas/other.md @@ -203,7 +203,7 @@ This file defines the schema for the OTHER vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishContinuationCluster": "PublishContinuationClusterRequest", diff --git a/play/engage-sdk-integration/references/schemas/read.md b/play/engage-sdk-integration/references/schemas/read.md index 698d707..bbaa2e3 100644 --- a/play/engage-sdk-integration/references/schemas/read.md +++ b/play/engage-sdk-integration/references/schemas/read.md @@ -395,7 +395,7 @@ This file defines the schema for the READ vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishContinuationCluster": "PublishContinuationClusterRequest", diff --git a/play/engage-sdk-integration/references/schemas/shopping.md b/play/engage-sdk-integration/references/schemas/shopping.md index e54edce..2b6f0d9 100644 --- a/play/engage-sdk-integration/references/schemas/shopping.md +++ b/play/engage-sdk-integration/references/schemas/shopping.md @@ -86,7 +86,7 @@ This file defines the schema for the SHOPPING vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishShoppingCart": "PublishShoppingCartClusterRequest", diff --git a/play/engage-sdk-integration/references/schemas/social.md b/play/engage-sdk-integration/references/schemas/social.md index 5aa01bf..5373317 100644 --- a/play/engage-sdk-integration/references/schemas/social.md +++ b/play/engage-sdk-integration/references/schemas/social.md @@ -217,7 +217,7 @@ This file defines the schema for the SOCIAL vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", "updatePublishStatus": "PublishStatusRequest", diff --git a/play/engage-sdk-integration/references/schemas/travel.md b/play/engage-sdk-integration/references/schemas/travel.md index 0cf70cc..d4a0f95 100644 --- a/play/engage-sdk-integration/references/schemas/travel.md +++ b/play/engage-sdk-integration/references/schemas/travel.md @@ -861,7 +861,7 @@ This file defines the schema for the TRAVEL vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishUserAccountManagementRequest": "PublishUserAccountManagementRequest", diff --git a/play/engage-sdk-integration/references/schemas/tv.md b/play/engage-sdk-integration/references/schemas/tv.md index 4d2f13f..7c5526f 100644 --- a/play/engage-sdk-integration/references/schemas/tv.md +++ b/play/engage-sdk-integration/references/schemas/tv.md @@ -1209,7 +1209,7 @@ This file defines the schema for the TV vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishContinuationCluster": "PublishContinuationClusterRequest", diff --git a/play/engage-sdk-integration/references/schemas/watch.md b/play/engage-sdk-integration/references/schemas/watch.md index 8e52fcc..381cab2 100644 --- a/play/engage-sdk-integration/references/schemas/watch.md +++ b/play/engage-sdk-integration/references/schemas/watch.md @@ -1120,7 +1120,7 @@ This file defines the schema for the WATCH vertical in the Engage SDK. } }, "methods": { - "isServiceAvailable": null, + "isServiceAvailable": "ServiceAvailabilityRequest", "publishRecommendationClusters": "PublishRecommendationClustersRequest", "publishFeaturedCluster": "PublishFeaturedClusterRequest", "publishContinuationCluster": "PublishContinuationClusterRequest", diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md index a9618d6..8d21d93 100644 --- a/wear/wear-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -9,7 +9,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-08' + last-updated: '2026-07-18' keywords: - Wear OS - Compose diff --git a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md index 70af9c6..9e1498d 100644 --- a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5: ### M3 - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha05") + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha06") See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). -Wear Compose Foundation library version 1.7.0-alpha05 introduced +Wear Compose Foundation library version 1.7.0-alpha06 introduced some new components that are designed to work with Material 3 components. Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an updated animation when running on Wear OS 6 (API level 36) or higher. When updating to Wear Compose Material 3 version, we suggest to also update the Wear Compose Foundation and Navigation libraries: - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha05") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha05") + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha06") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha06") ## Theme @@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components: And finally a list of some relevant components from Wear Compose Foundation library: -| Wear Compose Foundation 1.7.0-alpha05 | | +| Wear Compose Foundation 1.7.0-alpha06 | | |---|---| | [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | | [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 2a38ed0..1748d43 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-09' + last-updated: '2026-07-22' keywords: - Jetpack Compose Glimmer - audio glasses @@ -25,7 +25,7 @@ metadata: | Term | Definition | |---|---| | **Intelligent Eyewear** | All-day wear, hands-free devices that provide access to information. Equipped with speakers, a camera, and a microphone. Some are audio-only (audio glasses), and some also have a display (display glasses). | -| **Display Glasses** | Audio glasses with the addition of a small, private display for glanceable visuals that harmonize with audio output. | +| **Display glasses** | Audio glasses with the addition of a private display in lenses for glanceable visuals that harmonize with audio output. | | **Jetpack Compose Glimmer** | A Compose UI toolkit for building augmented Android XR experiences, optimized for display glasses. It provides components, theming, and behaviors for transparent displays. | | **Projected Activity (Glasses Activity)** | An Android `Activity` that runs on a host device (phone) but its UI and interactions are projected to a connected, intelligent eyewear device (audio or display glasses). | | **Projected Device** | An XR device connected to an Android-powered device (host). Host projects the application content to the Projected device and let users interact with it. | @@ -58,7 +58,7 @@ metadata: - **Setup Projected Activity:** First, you need to create a new projected activity for your app. If the project doesn't already have one, see [Create your first activity for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). Use [references/projectedcontext-source.md](references/projectedcontext-source.md) to launch the Glasses Projected activity on the Projected Device. Ensure that you specify `xr_projected` for the `android:requiredDisplayCategory` attribute in app manifest to tell the system that this activity will use a projected context to access hardware from a connected device. - **Mobile App Integration:** If the project contains an existing mobile app, you must create a new Glasses Activity dedicated to rendering Glimmer UI. For detailed configuration, heavily reference [Create your first activity - for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Always keep this button in a highly visible location, such as an overlay Floating Action Button (FAB) or the top navigation bar, to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the display glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. + for intelligent eyewear](https://developer.android.com/develop/xr/jetpack-xr-sdk/ai-glasses/first-activity). If there isn't already a method to launch the Glasses Activity, add a button to the existing mobile app UI labeled "Launch on Glasses" that uses `ProjectedContext` to launch the Glasses Activity on the glasses. Place the 'Launch on Glasses' button as a screen-level FloatingActionButton, in the TopAppBar, or in the top navigation bar to ensure users discover the projection capability. If the glasses aren't connected, disable the button. Don't launch the Glasses Activity on the phone, only on the display glasses. If it makes sense to automatically launch the Glasses Activity without an explicit launch button, then do so. - **UI Library:** Identify if the project has the `androidx.xr.glimmer:glimmer` library, if not it must be added to the project. See [Declaring Jetpack Compose Glimmer Dependencies](https://developer.android.com/jetpack/androidx/releases/xr-glimmer#declaring_dependencies) to fetch the latest dependency version. - **Theming:** All Glimmer components must be wrapped within the `GlimmerTheme` composable to ensure correct token resolution. - **Mandatory black background:** Display glasses use additive displays. Any non-black color in the background blocks the real world. **You must always** set a pure black background (`Modifier.background(Color.Black)`) on the root container of your Projected Activity. @@ -81,7 +81,7 @@ metadata: ## 3. Map input controls - Map app interactions, such as tap and swipe, to the available hardware controls on the glasses, such as the touchpad. -- Inputs are more 1-dimensional; users typically make one control input at a time. +- Inputs are one-dimensional; users must make one control input at a time. - Avoid nesting scrolling controls. - Jetpack Compose Glimmer components are designed to work with standard input methods, such as a tap or swipe on the glasses' touchpad. - Use System Back to dismiss temporary states or detailed views. @@ -217,7 +217,7 @@ concise information like a short title, a name, or a status. ##### Guidelines and usage -- **Spatial Spacing:** When using a standalone `TitleChip` above content, you must use `TitleChipDefaults.AssociatedContentSpacing` (8.dp) to maintain the visual hierarchy. +- **Spatial Spacing** : When using a standalone `TitleChip` preceding content, you must use `TitleChipDefaults.AssociatedContentSpacing` (8.dp) to maintain the visual hierarchy. - **Interactivity:** Title chips are purely for informational purposes, they cannot be targeted or activated for navigation. - **Layout** Always center text in a title chip. Never let the title chip go to two lines, and truncate extra words. Keep the label to three words or less. @@ -268,7 +268,7 @@ different types. - **Visual Consistency:** When building lists of similar items, always use a consistent background color (typically `GlimmerTheme.colors.surface`) and corner radius (standard 36.dp) for every item. Don't vary these unless you are visually grouping different *types* of content. - **Integrated Title Chips:** Glimmer Lists support integrated title chips. **IF** you need a section header within a list: **THEN** enable the integrated title chip rather than adding a standalone `TitleChip` to maintain spatial consistency. - **Vertical Arrangement:** ALWAYS use `verticalArrangement = - Arrangement.spacedBy(20.dp)` for `VerticalList` to ensure visual separation between items on the glasses display. + Arrangement.spacedBy(20.dp)` for `GlimmerLazyColumn` to ensure visual separation between items on the glasses display. - Be sure to use the default 20 dp spacing between list items unless otherwise specified. ##### Technical Documentation Links @@ -277,7 +277,7 @@ If you are creating a Glimmer List component, read the: - **API Source Code (List):** Use [references/list-source.md](references/list-source.md). - **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). -- **API Source Code (ListState):** Use [references/liststate-source.md](references/liststate-source.md). +- **API Source Code (GlimmerLazyListState):** Use [references/liststate-source.md](references/liststate-source.md). #### Stacks diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md index 437fadd..db9d205 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md @@ -108,8 +108,8 @@ public fun Modifier.depthEffect(depthEffect: DepthEffect?, shape: Shape): Modifi internal fun Modifier.depthEffect( from: DepthEffect?, to: DepthEffect?, - shap>e: Shape, - progress: () - Float, + shape: Shape, + progress: () -> Float, ): Modifier { // dropShadow draws the shadow, and then the content on top. So in order to get layer2 to // render on top of layer1, we draw layer1 first - this means that layer1's dropShadow will @@ -139,8 +139,8 @@ private fun ShadowScope.updateFrom(shadow: Shadow) { this.blendMode = shadow.blendMode } -private class DepthEffectElement(private val depthEffect: DepthEffect, private< val shape: Sha>pe) : - ModifierNodeElementDepthEffectNode() { +private class DepthEffectElement(private val depthEffect: DepthEffect, private val shape: Shape) : + ModifierNodeElement() { override fun create(): DepthEffectNode = DepthEffectNode(depthEffect, shape) @@ -216,10 +216,9 @@ internal class DepthEffectNode(private var depthEffect: DepthEffect, private var layer2ShadowPainter ?: requireGraphicsContext() .shadowContext - .createDropShadowPainter(shape,ayer2) + .createDropShadowPainter(shape, depthEffect.layer2) .also { layer2ShadowPainter = it } } -DepthEffect.kt ```
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md index 19b4604..325f09a 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md @@ -60,7 +60,7 @@ public fun GlimmerTheme( colors: Colors = GlimmerTheme.colors, typography: Typography = GlimmerTheme.typography, componentSpacingValues: ComponentSpacingValues = GlimmerTheme.componentSpacingValues, - conte>nt: @Composable () - Unit, + content: @Composable () -> Unit, ) { val theme = GlimmerTheme(colors, typography, componentSpacingValues) CompositionLocalProvider( @@ -136,7 +136,7 @@ public class GlimmerTheme( * retrieving values from inside CompositionLocalConsumerModifierNode implementations - in * most cases you should use [colors] and other properties directly. */ - pualGlimmerTheme: CompositionLocalGlimmerTheme + public val LocalGlimmerTheme: CompositionLocal get() = _localGlimmerTheme /** @@ -173,11 +173,10 @@ private object NoIndication : IndicationNodeFactory { /** Use [GlimmerTheme.LocalGlimmerTheme] to access this publicly. */ @Suppress("CompositionLocalNaming") -privalGlimmerTheme: ProvidableCompositionLocalGlimmerTheme = - stLocalOf { +private val _localGlimmerTheme: ProvidableCompositionLocal = + staticCompositionLocalOf { GlimmerTheme() } -GlimmerTheme.kt ```
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md index cc8400f..f213e12 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/list-source.md @@ -1,392 +1,25 @@ When creating a Glimmer List component, refer to the following source code in -`List.kt`: +`GlimmerLazyColumnSamples.kt`: ```kotlin -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.xr.glimmer.list - -import androidx.compose.foundation.OverscrollEffect -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.layout.LazyLayout -import androidx.compose.foundation.rememberOverscrollEffect -import androidx.compose.foundation.scrollableArea -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastMap -import androidx.xr.glimmer.GlimmerTheme -import androidx.xr.glimmer.edgeScrim -import kotlin.math.max - -/** - * This is a scrolling list component that only composes and lays out the currently visible items. - * It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and - * customized behavior required for Jetpack Compose Glimmer. For Jetpack Compose Glimmer - * applications, it is recommended to use [VerticalList] instead of - * [androidx.compose.foundation.lazy.LazyColumn], as it is specifically designed to provide seamless - * focus-based navigation, visual scrim edge effects and support for focus-aware snap behavior. - * - * The [content] block defines a DSL which allows you to emit items of different types. For example, - * you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items. - * - * See the other [VerticalList] overload for a variant with a title slot. - * - * @sample androidx.xr.glimmer.samples.VerticalListSample - * @param modifier the modifier to apply to this layout. - * @param state the state object to be used to control or observe the list's state. - * @param contentPadding a padding around the whole content. This will add padding for the content - * after it has been clipped, which is not possible via [modifier] param. You can use it to add a - * padding before the first item or after the last one. - * @param userScrollEnabled If user gestures are enabled. - * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this - * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not - * need to use Modifier.overscroll separately. - * @param flingBehavior logic describing fling and snapping behavior when drag has finished. - * @param reverseLayout reverses the direction of scrolling and layout. - * @param horizontalAlignment aligns items horizontally. - * @param verticalArrangement is arrangement for items. This only applies if the content is smaller - * than the viewport. - * @param content a block which describes the content. Inside this block you can use methods like - * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. - */ @Composable -public fun VerticalList( - modifier: Modifier = Modifier, - state: ListState = rememberListState(), - contentPadding: PaddingValues = VerticalListDefaults.contentPadding, - userScrollEnabled: Boolean = true, - overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), - flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state), - reverseLayout: Boolean = false, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement, - content: ListScope.() -> Unit, -): Unit = - List( - orientation = Orientation.Vertical, - modifier = modifier, - state = state, - reverseLayout = reverseLayout, - contentPadding = contentPadding, - userScrollEnabled = userScrollEnabled, - overscrollEffect = overscrollEffect, - flingBehavior = flingBehavior, - horizontalAlignment = horizontalAlignment, - verticalArrangement = verticalArrangement, - verticalAlignment = null, - horizontalArrangement = null, - content = content, - ) - -/** - * This is a scrolling list component that only composes and lays out the currently visible items. - * It is based on [androidx.compose.foundation.lazy.LazyColumn], but with extra functionality and - * customized behavior required for Jetpack Compose Glimmer. Jetpack Compose Glimmer applications - * should always use VerticalList instead of LazyColumn to ensure correct behavior. - * - * The [content] block defines a DSL which allows you to emit items of different types. For example, - * you can use [ListScope.item] to add a single item and [ListScope.items] to add a list of items. - * - * This overload of `VerticalList` contains a `title` slot. The title is expected to be a - * [androidx.xr.glimmer.TitleChip]. It is positioned at the top center and visually overlaps the - * list content. The list is vertically offset to start from the title's vertical center. When the - * list is scrolled, the title remains static. - * - * See the other [VerticalList] overload for a variant with no title slot. - * - * @sample androidx.xr.glimmer.samples.VerticalListWithTitleChipSample - * @param title a composable slot for the list title, expected to be a - * [androidx.xr.glimmer.TitleChip]. It overlaps the list, positioned at the top-center, and - * remains stuck to the top when the list is scrolled. - * @param modifier applies to the layout that contains both list and title. - * @param state the state object to be used to control or observe the list's state. - * @param contentPadding a padding around the whole content. This will add padding for the content - * after it has been clipped, which is not possible via [modifier] param. You can use it to add a - * padding before the first item or after the last one. The list is vertically offset to start - * from the title's vertical center, so custom content paddings must provide sufficient space to - * avoid content being obscured. - * @param userScrollEnabled If user gestures are enabled. - * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this - * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not - * need to use Modifier.overscroll separately. - * @param flingBehavior logic describing fling and snapping behavior when drag has finished. - * @param reverseLayout reverses the direction of scrolling and layout. - * @param horizontalAlignment aligns items horizontally. - * @param verticalArrangement is arrangement for items. This only applies if the content is smaller - * than the viewport. - * @param content a block which describes the content. Inside this block you can use methods like - * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. - */ -@Suppress( - // The main trailing lambda is [content], but it's DSL. - "ComposableLambdaParameterNaming", - "ComposableLambdaParameterPosition", -) -@Composable -public fun VerticalList( - title: @Composable () -> Unit, - modifier: Modifier = Modifier, - state: ListState = rememberListState(), - contentPadding: PaddingValues = VerticalListDefaults.contentPaddingWithTitle, - userScrollEnabled: Boolean = true, - overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), - flingBehavior: FlingBehavior = VerticalListDefaults.flingBehavior(state), - reverseLayout: Boolean = false, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - verticalArrangement: Arrangement.Vertical = VerticalListDefaults.verticalArrangement, - content: ListScope.() -> Unit, -) { - VerticalListWithTitleLayout( - modifier = modifier, - title = title, - list = { - VerticalList( - state = state, - contentPadding = contentPadding, - userScrollEnabled = userScrollEnabled, - overscrollEffect = overscrollEffect, - flingBehavior = flingBehavior, - reverseLayout = reverseLayout, - horizontalAlignment = horizontalAlignment, - verticalArrangement = verticalArrangement, - content = content, - ) - }, - ) -} - -/** Contains the default values used by [VerticalList]. */ -public object VerticalListDefaults { - /** - * Recommended value for the distance between items. - * - * @see [verticalArrangement] for the default arrangement that uses this spacing. - */ - public val itemSpacing: Dp - @Composable get() = GlimmerTheme.componentSpacingValues.extraLarge - - /** The maximum height of the fade effects on the sides of the list. */ - public val ScrimMaxHeight: Dp = 46.dp - - /** Recommended content padding values for lists without a title. */ - public val contentPadding: PaddingValues - @Composable get() = PaddingValues(vertical = itemSpacing, horizontal = 0.dp) - - /** Recommended content padding values for lists with a title. */ - public val contentPaddingWithTitle: PaddingValues - @Composable get() = PaddingValues(top = ScrimMaxHeight, bottom = itemSpacing) - - /** Recommended values for the vertical arrangement. */ - public val verticalArrangement: Arrangement.Vertical - @Composable get() = Arrangement.spacedBy(itemSpacing) - - /** - * Creates and remembers the default fling behavior for a [VerticalList] that aligns the focus - * position with list scroll. - * - * @param state The [ListState] to observe for layout and focus information. - * @return A [FlingBehavior] instance that provides focus-aware snapping. - */ - @Composable - public fun flingBehavior(state: ListState): FlingBehavior { - val snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) } - return rememberSnapFlingBehavior(snapLayoutInfoProvider) +fun GlimmerLazyColumnSample() { + GlimmerLazyColumn { + item { ListItem { Text("Header") } } + items(count = 10) { index -> ListItem { Text("Item-$index") } } + item { ListItem { Text("Footer") } } } } +``` +```kotlin @Composable -private fun VerticalListWithTitleLayout( - modifier: Modifier = Modifier, - title: @Composable () -> Unit, - list: @Composable () -> Unit, -) { - Layout(modifier = modifier, contents = listOf(list, title)) { measurables, constraints -> - // The title parameter is provided by users, requiring iteration through all measurables. - // The list parameter is provided by us, allowing to guarantee that it contains only - // a single measurable. - val listMeasurable = measurables[0][0] - val titleMeasurables = measurables[1] - // Measure title(s) first. - var titleMaxHeight = 0 - var titleMaxWidth = 0 - val titleConstraints = constraints.copyMaxDimensions() - val titlePlaceables = - titleMeasurables.fastMap { measurable -> - val placeable = measurable.measure(titleConstraints) - titleMaxHeight = max(titleMaxHeight, placeable.height) - titleMaxWidth = max(titleMaxWidth, placeable.width) - placeable - } - - // List shouldn't use the space above the vertical center of the title. - val titleYOffset = titleMaxHeight / 2 - val maxListHeight = constraints.maxHeight - titleYOffset - val minListHeight = minOf(constraints.minHeight, maxListHeight) - val listConstraints = constraints.copy(minHeight = minListHeight, maxHeight = maxListHeight) - val listPlaceable = listMeasurable.measure(listConstraints) - - val layoutWidth = maxOf(listPlaceable.width, titleMaxWidth) - val layoutHeight = listPlaceable.height + titleYOffset - layout(width = layoutWidth, height = layoutHeight) { - // Place the list first. - listPlaceable.placeRelative( - x = (layoutWidth - listPlaceable.width) / 2, - y = titleYOffset, - ) - // Then place the rest of the titles on top of the list. - titlePlaceables.fastForEach { titlePlaceable -> - // Each title's center aligned with the top of the list. - titlePlaceable.placeRelative( - x = (layoutWidth - titlePlaceable.width) / 2, - y = (titleMaxHeight - titlePlaceable.height) / 2, - ) - } - } - } -} - -/** - * The scrolling list that only composes and lays out the currently visible items. The [content] - * block defines a DSL which allows you to emit items of different types. For example, you can use - * [ListScope.item] to add a single item and [ListScope.items] to add a list of items. - * - * @param orientation The orientation in which to layout items in this list. - * @param modifier the modifier to apply to this layout. - * @param state the state object to be used to control or observe the list's state. - * @param contentPadding a padding around the whole content. This will add padding for the content - * after it has been clipped, which is not possible via [modifier] param. You can use it to add a - * padding before the first item or after the last one. - * @param userScrollEnabled If user gestures are enabled. - * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this - * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not - * need to use Modifier.overscroll separately. - * @param flingBehavior logic describing fling and snapping behavior when drag has finished. - * @param reverseLayout reverses the direction of scrolling and layout. - * @param horizontalAlignment aligns items horizontally. It's required and used only if - * [orientation] is [Orientation.Vertical]. - * @param verticalArrangement is arrangement for items. This only applies if the content is smaller - * than the viewport. It's required and used only if [orientation] is [Orientation.Vertical]. - * @param verticalAlignment aligns items vertically. It's required and used only if [orientation] is - * [Orientation.Horizontal]. - * @param horizontalArrangement is arrangement for items. This only applies if the content is - * smaller than the viewport. It's required and used only if [orientation] is - * [Orientation.Vertical]. - * @param content a block which describes the content. Inside this block you can use methods like - * [ListScope.item] to add a single item or [ListScope.items] to add a list of items. - */ -@Composable -internal fun List( - orientation: Orientation, - modifier: Modifier, - state: ListState, - contentPadding: PaddingValues, - userScrollEnabled: Boolean, - overscrollEffect: OverscrollEffect?, - flingBehavior: FlingBehavior, - reverseLayout: Boolean, - horizontalAlignment: Alignment.Horizontal?, - verticalArrangement: Arrangement.Vertical?, - verticalAlignment: Alignment.Vertical?, - horizontalArrangement: Arrangement.Horizontal?, - content: ListScope.() -> Unit, -) { - val itemProvider = rememberGlimmerListItemProviderLambda(state, content) - - val semanticState = rememberGlimmerListSemanticState(state, orientation) - - val scrollEnabled = isScrollEnabled(userScrollEnabled, state) - - val measurePolicy = - rememberGlimmerListMeasurePolicy( - itemProviderLambda = itemProvider, - state = state, - contentPadding = contentPadding, - reverseLayout = reverseLayout, - orientation = orientation, - horizontalAlignment = horizontalAlignment, - verticalArrangement = verticalArrangement, - verticalAlignment = verticalAlignment, - horizontalArrangement = horizontalArrangement, - ) - - val beyondBoundsModifier = - if (scrollEnabled) { - Modifier.lazyLayoutBeyondBoundsModifier( - state = rememberGlimmerListBeyondBoundsState(state), - beyondBoundsInfo = state.beyondBoundsInfo, - reverseLayout = reverseLayout, - orientation = orientation, - ) - } else { - Modifier - } - - LazyLayout( - modifier = - modifier - .then(state.remeasurementModifier) - .then(state.awaitLayoutModifier) - .autoFocus(state.autoFocusState) - .lazyLayoutSemantics( - itemProviderLambda = itemProvider, - state = semanticState, - orientation = orientation, - userScrollEnabled = scrollEnabled, - reverseScrolling = reverseLayout, - ) - .then(beyondBoundsModifier) - .edgeScrim( - state = state.scrollIndicatorState, - orientation = orientation, - maxScrimSize = VerticalListDefaults.ScrimMaxHeight, - ) - .scrollableArea( - state = state, - orientation = orientation, - enabled = scrollEnabled, - interactionSource = state.internalInteractionSource, - overscrollEffect = overscrollEffect, - flingBehavior = flingBehavior, - ), - itemProvider = itemProvider, - measurePolicy = measurePolicy, - ) -} - -@Composable -private fun isScrollEnabled(userScrollEnabled: Boolean, state: ListState): Boolean { - if (userScrollEnabled) { - val derivedState = - remember(state) { derivedStateOf { state.canScrollForward || state.canScrollBackward } } - return derivedState.value - } else { - return false +fun GlimmerLazyColumnWithTitleChipSample() { + val ingredientItems = + listOf("Milk", "Flour", "Egg", "Salt", "Apples", "Butter", "Vanilla", "Sugar", "Cinnamon") + GlimmerLazyColumn(title = { TitleChip { Text("Ingredients") } }) { + items(ingredientItems) { text -> ListItem { Text(text) } } } } ``` diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md index 134df7c..664dd0a 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md @@ -1,5 +1,5 @@ When creating a Glimmer List component, refer to the following source code in -`ListState.kt` for creating a state for the list: +`GlimmerLazyListState.kt` for creating a state for the list: ```kotlin @@ -45,46 +45,49 @@ import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density -import androidx.xr.glimmer.list.ListState.Companion.Saver +import androidx.xr.glimmer.list.GlimmerLazyListState.Companion.Saver import kotlin.math.abs /** - * Creates a [ListState] that is remembered across compositions. + * Creates a [GlimmerLazyListState] that is remembered across compositions. * * Changes to the provided initial values will **not** result in the state being recreated or * changed in any way if it has already been created. * - * @param initialFirstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param initialFirstVisibleItemIndex the initial value for + * [GlimmerLazyListState.firstVisibleItemIndex] * @param initialFirstVisibleItemScrollOffset the initial value for - * [ListState.firstVisibleItemScrollOffset] + * [GlimmerLazyListState.firstVisibleItemScrollOffset] */ @Composable -public fun rememberListState( +public fun rememberGlimmerLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0, -): ListState = - rememberSaveable(saver = ListState.Saver) { - ListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) +): GlimmerLazyListState = + rememberSaveable(saver = GlimmerLazyListState.Saver) { + GlimmerLazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) } /** * A state object that can be hoisted to control and observe scrolling. * - * In most cases, this will be created via [rememberListState]. + * In most cases, this will be created via [rememberGlimmerLazyListState]. * - * @param firstVisibleItemIndex the initial value for [ListState.firstVisibleItemIndex] + * @param firstVisibleItemIndex the initial value for [GlimmerLazyListState.firstVisibleItemIndex] * @param firstVisibleItemScrollOffset the initial value for - * [ListState.firstVisibleItemScrollOffset] + * [GlimmerLazyListState.firstVisibleItemScrollOffset] */ -public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) : - ScrollableState { +public class GlimmerLazyListState( + firstVisibleItemIndex: Int = 0, + firstVisibleItemScrollOffset: Int = 0, +) : ScrollableState { private val backingState = ScrollableState { -onScroll(-it) } // TODO: b/414961654 - Consider making this abstraction around "anchor item". /** The holder class for the current scroll position. */ private val scrollPosition = - GlimmerListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) + GlimmerLazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) /** Backing state for [layoutInfo] */ internal val layoutInfoState = mutableStateOf(EmptyLazyListMeasureResult, neverEqualPolicy()) @@ -101,7 +104,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() /** Includes information for requesting focus for children as the list scrolls. */ - internal val autoFocusState = GlimmerListAutoFocusState() + internal val autoFocusState = GlimmerLazyListAutoFocusState() /** Stores currently pinned items which are always composed. */ internal val pinnedItems = LazyLayoutPinnedItemList() @@ -159,7 +162,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff internal val remeasurementModifier = object : RemeasurementModifier { override fun onRemeasurementAvailable(remeasurement: Remeasurement) { - this@ListState.remeasurement = remeasurement + this@GlimmerLazyListState.remeasurement = remeasurement } } @@ -172,7 +175,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff /** * The index of the first item that is visible within the scrollable viewport area not including * items in the content padding region. For the first visible item that includes items in the - * content padding please use [ListLayoutInfo.visibleItemsInfo]. + * content padding please use [GlimmerLazyListLayoutInfo.visibleItemsInfo]. * * Note that this property is observable and if you use it in the composable function it will be * recomposed on every change causing potential performance issues. @@ -191,8 +194,8 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff @FrequentlyChangingValue get() = scrollPosition.scrollOffset /** - * The object of [ListLayoutInfo] calculated during the last layout pass. For example, you can - * use it to calculate what items are currently visible. + * The object of [GlimmerLazyListLayoutInfo] calculated during the last layout pass. For + * example, you can use it to calculate what items are currently visible. * * Note that this property is observable and is updated after every scroll or remeasure. If you * use it in the composable function it will be recomposed on every change causing potential @@ -202,7 +205,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff * If you want to run some side effects like sending an analytics event or updating a state * based on this value consider using "snapshotFlow": */ - public val layoutInfo: ListLayoutInfo + public val layoutInfo: GlimmerLazyListLayoutInfo @FrequentlyChangingValue get() = layoutInfoState.value /** @@ -225,7 +228,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff * visible one even given that its index has been changed. */ internal fun updateScrollPositionIfTheFirstItemWasMoved( - itemProvider: GlimmerListItemProvider, + itemProvider: GlimmerLazyListItemProvider, firstItemIndex: Int, ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) @@ -242,7 +245,7 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff * [consumedScroll]. */ internal fun applyMeasureResult( - result: GlimmerListMeasureResult, + result: GlimmerLazyListMeasureResult, consumedScroll: Float, scrollToCarryOver: Float, ) { @@ -314,8 +317,9 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff if (this === EmptyLazyListMeasureResult) { Int.MAX_VALUE } else { - this@ListState.firstVisibleItemIndex * visibleItemsAverageSize + - this@ListState.firstVisibleItemScrollOffset + this@GlimmerLazyListState.firstVisibleItemIndex * + visibleItemsAverageSize + + this@GlimmerLazyListState.firstVisibleItemScrollOffset } } @@ -370,25 +374,28 @@ public class ListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOff */ public suspend fun animateScrollToItem(@IntRange(from = 0) index: Int, scrollOffset: Int = 0) { scroll { - GlimmerListScrollScope(this@ListState, this) + GlimmerLazyListScrollScope(this@GlimmerLazyListState, this) .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density) } } public companion object { - /** The default [Saver] implementation for [ListState]. */ - public val Saver: Saver = + /** The default [Saver] implementation for [GlimmerLazyListState]. */ + public val Saver: Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { - ListState(firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1]) + GlimmerLazyListState( + firstVisibleItemIndex = it[0], + firstVisibleItemScrollOffset = it[1], + ) }, ) } } private val EmptyLazyListMeasureResult = - GlimmerListMeasureResult( + GlimmerLazyListMeasureResult( firstVisibleItem = null, firstVisibleItemScrollOffset = 0, canScrollForward = false, From 23d9eae21a4bfe0209e5b678f0ebe931e3c7dff4 Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:37:25 +0000 Subject: [PATCH 35/61] Fix Claude Code and Codex plugin/marketplace manifests (#113) * Fix Claude Code and Codex plugin/marketplace manifests * Fix Claude Code and Codex plugin/marketplace manifests --- .claude-plugin/marketplace.json | 4 ++-- .codex-plugin/plugin.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b217d93..e9a2cee 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -14,7 +14,7 @@ "source": "./", "strict": false, "skills": [ - "./build/agp/agp-9-upgrade", + "./build-system/agp/agp-9-upgrade", "./camera/camerax", "./device-ai/appfunctions", "./devtools/android-cli", @@ -32,7 +32,7 @@ "./security/android-intent-security", "./system/edge-to-edge", "./testing/testing-setup", - "./wear/jetpack-compose-m3", + "./wear/wear-compose-m3", "./xr/display-glasses-with-jetpack-compose-glimmer" ], "homepage": "https://github.com/android/skills", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index d83d925..506a95a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "android-skills", - "version": "1.0.5", + "version": "1.0.6", "description": "Android skills are Google's official dedicated repository of AI-optimized, modular instructions and resources, to help LLMs better understand and execute specific patterns that follow the best practices and guidance on Android development. Android skills follow the open-standard agent skills - markdown files (SKILL.md) that provide a technical specification of a task, and ground LLMs with information on specialized domains and workflows.", "author": { "name": "Google / Android", @@ -17,7 +17,7 @@ "skills": [ { "source": { - "path": "./build/agp/agp-9-upgrade" + "path": "./build-system/agp/agp-9-upgrade" } }, { @@ -107,7 +107,7 @@ }, { "source": { - "path": "./wear/jetpack-compose-m3" + "path": "./wear/wear-compose-m3" } }, { From ba0042c08b7e6ff5cb121b7b87d442f809467324 Mon Sep 17 00:00:00 2001 From: Vinisha Athwani <35296563+vinishavathwani@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:07:16 +0530 Subject: [PATCH 36/61] Update readMe (#121) * Update README.md * Update README.md * Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55ef253..1490f88 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Android skills **Android skills** are a dedicated repository of **AI-optimized, modular instructions** and -resources, to help LLMs better understand and execute specific patterns that follow the best +resources, to help agents better understand and execute specific patterns that follow the best practices and guidance on Android development from [developer.android.com](https://developer.android.com). @@ -47,6 +47,10 @@ will be installed for Gemini and Antigravity at `~/.gemini/antigravity/skills`. only the `android-cli` skill will be installed. - `--project` - Path to a project root in which to install the skills. +### Activate skills + +Your agent automatically activates skills that are relevant to your task. To use a skill, prompt your agent to complete a task that is related to the skill, for example "Make my app UI edge-to-edge." The agent should automatically find and use the skill if it has been installed. + ## Disclaimer AI can make mistakes, so always double-check the results. From 6fd7e4d545f9013d5f2e8acc9e04114b26ef92df Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:51:25 +0000 Subject: [PATCH 37/61] Update workflows with agent marketplace generation files (#124) * Update workflows with agent marketplace generation files * Fix failing zizmor errors * Modify update-skills steps to update agent marketplace files and skill lists with every new update * Modify update-skills steps to update agent marketplace files and skill lists with every new update * Resolve comments --- .github/workflows/create-release.yml | 42 +++++++++++++++++++++++++-- .github/workflows/update-skills.yml | 43 +++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 92c9029..0a0c28d 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -16,9 +16,41 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout main - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main + token: ${{ secrets.ADR_GITHUB_BOT_PAT || secrets.GITHUB_TOKEN }} + + - name: Update Codex Plugin Version + env: + TAG_NAME: ${{ inputs.tag_name }} + run: | + VERSION="${TAG_NAME#v}" + echo "Updating .codex-plugin/plugin.json version to $VERSION" + + VERSION="$VERSION" python3 -c " + import json + import os + + version = os.environ['VERSION'] + path = '.codex-plugin/plugin.json' + with open(path, 'r') as f: + data = json.load(f) + data['version'] = version + with open(path, 'w') as f: + json.dump(data, f, indent=2) + f.write('\n') + " + + - name: Commit and push updated version + env: + TAG_NAME: ${{ inputs.tag_name }} + run: | + git config user.name "android-devrel-github-bot" + git config user.email "android-devrel-github-bot@users.noreply.github.com" + git add .codex-plugin/plugin.json + git commit -m "Bump plugin version to $TAG_NAME" || echo "No version change to commit" + git push origin main - name: Create Zip Archive run: | @@ -26,6 +58,10 @@ jobs: - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ADR_GITHUB_BOT_PAT || secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ inputs.tag_name }} run: | - gh release create ${{ inputs.tag_name }} android-skills.zip --title "Release ${{ inputs.tag_name }}" --notes "Manual release of main branch contents." + gh release create "$TAG_NAME" android-skills.zip \ + --target main \ + --title "Release $TAG_NAME" \ + --notes "Release $TAG_NAME of Android Skills." diff --git a/.github/workflows/update-skills.yml b/.github/workflows/update-skills.yml index 08bf523..47afa74 100644 --- a/.github/workflows/update-skills.yml +++ b/.github/workflows/update-skills.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout main - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main token: ${{ secrets.ADR_GITHUB_BOT_PAT }} @@ -59,6 +59,47 @@ jobs: rm -rf temp_skills echo "Successfully copied directories from github-skills branch." + - name: Update Skill Lists in Plugin Files + run: | + python3 -c " + import json + import pathlib + + # Discover all skills with SKILL.md + skill_paths = [] + for path in pathlib.Path('.').glob('**/SKILL.md'): + if any(p.startswith('.') for p in path.parts[:-1]): + continue + skill_paths.append(f'./{path.parent.as_posix()}') + + skill_paths = sorted(list(set(skill_paths))) + + # 1. Update .claude-plugin/marketplace.json in-place + claude_file = pathlib.Path('.claude-plugin/marketplace.json') + if not claude_file.exists(): + raise FileNotFoundError(f'Required file {claude_file} does not exist. Ensure you are running on the correct branch.') + + with open(claude_file, 'r') as f: + data = json.load(f) + if 'plugins' in data and len(data['plugins']) > 0: + data['plugins'][0]['skills'] = skill_paths + with open(claude_file, 'w') as f: + json.dump(data, f, indent=2) + f.write('\n') + + # 2. Update .codex-plugin/plugin.json in-place + codex_file = pathlib.Path('.codex-plugin/plugin.json') + if not codex_file.exists(): + raise FileNotFoundError(f'Required file {codex_file} does not exist. Ensure you are running on the correct branch.') + + with open(codex_file, 'r') as f: + data = json.load(f) + data['skills'] = [{'source': {'path': p}} for p in skill_paths] + with open(codex_file, 'w') as f: + json.dump(data, f, indent=2) + f.write('\n') + " + - name: Commit and push changes run: | git config user.name "android-devrel-github-bot" From 9f80a958c4838def2e0c4d5da709f479aa090bd3 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Tue, 4 Aug 2026 14:51:56 +0000 Subject: [PATCH 38/61] Updates skills (2026-08-04 14:51) --- .claude-plugin/marketplace.json | 2 +- .codex-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e9a2cee..d8900b3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -39,4 +39,4 @@ "license": "Apache-2.0" } ] -} \ No newline at end of file +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 506a95a..f28a37b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -131,4 +131,4 @@ "Migrate this Camera1 code to CameraX." ] } -} \ No newline at end of file +} From 63123be36fcc16c447c35a9c9a3423bc0efd7e50 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Tue, 4 Aug 2026 14:53:55 +0000 Subject: [PATCH 39/61] Updates skills (2026-08-04 14:53) --- .claude-plugin/marketplace.json | 1 - .codex-plugin/plugin.json | 5 - device-ai/appfunctions/SKILL.md | 6 +- .../appfunctions/{ => references}/context.md | 0 identity/verified-email/SKILL.md | 14 +- .../email-verification-implementation.md | 24 +- .../digital-credentials/email-verification.md | 145 -- .../identity/passkeys/create-passkeys.md | 8 +- jetpack-compose/adaptive/SKILL.md | 2 +- .../layouts/adaptive/flexbox/get-started.md | 2 +- .../layouts/adaptive/grid/get-started.md | 2 +- performance/r8-analyzer/SKILL.md | 37 +- .../references/CONFIGURATION-ANALYZER.md | 287 ---- .../references/REFLECTION-GUIDE.md | 139 -- play/engage-sdk-integration/SKILL.md | 8 +- .../android/guide/playcore/engage/social.md | 702 ++++++++++ .../playcore/engage/tv/getting-started.md | 16 +- .../references/common.md | 519 ------- .../SKILL.md | 93 -- .../google/play/billing/release-notes.md | 1226 ----------------- .../references/migration-logic.md | 78 -- .../references/version-checklist.md | 54 - wear/wear-compose-m3/SKILL.md | 165 ++- .../wearables/compose/migrate-to-material3.md | 10 +- .../foundation/CurvedWorldSample.kt.md.txt | 430 ++++++ .../foundation/ExpandableSample.kt.md.txt | 99 ++ .../HierarchicalFocusSample.kt.md.txt | 141 ++ .../foundation/PagerSamples.kt.md.txt | 56 + .../foundation/RotarySamples.kt.md.txt | 153 ++ .../ScalingLazyColumnSample.kt.md.txt | 131 ++ .../SwipeToDismissBoxSample.kt.md.txt | 168 +++ .../foundation/SwipeToRevealSample.kt.md.txt | 282 ++++ .../TransformingLazyColumnSample.kt.md.txt | 335 +++++ .../material3/AlertDialogSample.kt.md.txt | 377 +++++ .../material3/AnimatedTextSample.kt.md.txt | 165 +++ .../material3/ButtonGroupSample.kt.md.txt | 91 ++ .../material3/ButtonSample.kt.md.txt | 496 +++++++ .../references/material3/CardSample.kt.md.txt | 470 +++++++ .../material3/CheckboxButtonSample.kt.md.txt | 69 + .../ConfirmationDialogSample.kt.md.txt | 162 +++ .../material3/CurvedTextSamples.kt.md.txt | 78 ++ .../material3/DatePickerSample.kt.md.txt | 131 ++ .../DynamicColorSchemeSample.kt.md.txt | 74 + .../material3/EdgeButtonSample.kt.md.txt | 63 + .../FadingExpandingLabelSample.kt.md.txt | 60 + .../material3/IconButtonSample.kt.md.txt | 125 ++ .../IconToggleButtonSample.kt.md.txt | 116 ++ .../material3/LevelIndicatorSample.kt.md.txt | 68 + .../material3/ListHeaderSample.kt.md.txt | 177 +++ .../OneHandedGestureSamples.kt.md.txt | 651 +++++++++ .../OpenOnPhoneDialogSample.kt.md.txt | 57 + .../material3/PageIndicatorSample.kt.md.txt | 110 ++ .../material3/PagerScaffoldSample.kt.md.txt | 204 +++ .../material3/PickerSample.kt.md.txt | 111 ++ .../material3/PlaceholderSample.kt.md.txt | 182 +++ .../ProgressIndicatorSample.kt.md.txt | 263 ++++ .../material3/RadioButtonSample.kt.md.txt | 109 ++ .../material3/ScaffoldSample.kt.md.txt | 146 ++ .../material3/ScrollAwaySample.kt.md.txt | 97 ++ .../material3/ScrollIndicatorSample.kt.md.txt | 66 + .../material3/SliderSample.kt.md.txt | 86 ++ .../material3/StepperSample.kt.md.txt | 148 ++ .../SurfaceTransformationSample.kt.md.txt | 165 +++ .../SwipeToDismissBoxSample.kt.md.txt | 171 +++ .../material3/SwitchButtonSample.kt.md.txt | 68 + .../material3/TextButtonSample.kt.md.txt | 115 ++ .../TextToggleButtonSample.kt.md.txt | 71 + .../material3/TimeTextSample.kt.md.txt | 65 + .../TransformationSpecSample.kt.md.txt | 246 ++++ ...ingLazyColumnNotificationsSample.kt.md.txt | 89 ++ .../TransformingLazyColumnSample.kt.md.txt | 309 +++++ .../SKILL.md | 8 +- .../request-hardware-permissions.md | 179 ++- .../references/icon-source.md | 389 ------ .../references/listitem-source.md | 281 ---- 75 files changed, 9043 insertions(+), 3403 deletions(-) rename device-ai/appfunctions/{ => references}/context.md (100%) delete mode 100644 identity/verified-email/references/android/identity/digital-credentials/email-verification.md delete mode 100644 performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md delete mode 100644 performance/r8-analyzer/references/REFLECTION-GUIDE.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/social.md delete mode 100644 play/engage-sdk-integration/references/common.md delete mode 100644 play/play-billing-library-version-upgrade/SKILL.md delete mode 100644 play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md delete mode 100644 play/play-billing-library-version-upgrade/references/migration-logic.md delete mode 100644 play/play-billing-library-version-upgrade/references/version-checklist.md create mode 100644 wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CardSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md delete mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d8900b3..fa5e1e5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -25,7 +25,6 @@ "./navigation/navigation-3", "./performance/r8-analyzer", "./play/engage-sdk-integration", - "./play/play-billing-library-version-upgrade", "./play/play-policy-insights", "./profilers/perfetto-sql", "./profilers/perfetto-trace-analysis", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f28a37b..637f0b7 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -70,11 +70,6 @@ "path": "./play/engage-sdk-integration" } }, - { - "source": { - "path": "./play/play-billing-library-version-upgrade" - } - }, { "source": { "path": "./play/play-policy-insights" diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md index ad75915..cc5a2c6 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-07-28' + last-updated: '2026-07-31' keywords: - AppFunctions - Kotlin @@ -55,7 +55,7 @@ If users request a subset of steps, you must encourage them to use all steps. If they apply, you must load the following references: -- *[Context and terminology](context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. +- *[Context and terminology](references/context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. - *[Migration to service entry point](references/migrate-to-service-entry-point.md)* : Documents the systematic procedure for migrating Android applications using AppFunctions versions 1.0.0-alpha09 and lower to `AppFunctionServiceEntryPoint` architecture introduced in version 1.0.0-alpha10. Load when a user asks to migrate or upgrade existing AppFunctions code, or when you encounter legacy `AppFunctionConfiguration.Provider` implementations. ## Critical constraints @@ -67,5 +67,5 @@ If they apply, you must load the following references: - If you encounter build-time errors such as Kotlin Symbol Processing (KSP) issues, see [Implementation and configuration](references/implementation-configuration.md). - If you encounter runtime errors such as missing services or execution failures, see [Testing and debugging](references/adb-interaction-testing.md). -- If you need architecture definitions and vocabulary, see [Context and terminology](context.md). +- If you need architecture definitions and vocabulary, see [Context and terminology](references/context.md). - If you encounter issues when upgrading legacy configurations, see [Migration to service entry point](references/migrate-to-service-entry-point.md). diff --git a/device-ai/appfunctions/context.md b/device-ai/appfunctions/references/context.md similarity index 100% rename from device-ai/appfunctions/context.md rename to device-ai/appfunctions/references/context.md diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 784038e..b331f3e 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-07-02' + last-updated: '2026-07-30' keywords: - implementation - Android @@ -26,7 +26,7 @@ metadata: ## Fundamentals -- *[Overview of Digital Credentials](references/android/identity/digital-credentials/email-verification.md)*: Learn about cryptographically verifiable documents and the role of Credential Manager. +- *[Overview of Digital Credentials](https://developer.android.com/identity/digital-credentials/email-verification)*: Learn about cryptographically verifiable documents and the role of Credential Manager. - *[Glossary](references/android/identity/digital-credentials/email-verification-implementation.md)* : Definitions for `dcql_query`, `UserInfoCredential`, and `GetDigitalCredentialOption`. ### Standards \& Examples @@ -130,8 +130,8 @@ Manager: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -139,8 +139,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` @@ -404,7 +404,7 @@ actual call to the Credential Manager API. ## See also -- [Overview of verified email retrieval](references/android/identity/digital-credentials/email-verification.md) +- [Overview of verified email retrieval](https://developer.android.com/identity/digital-credentials/email-verification) - [Credential Manager](references/android/identity/credential-manager/index.md) ## Critical Security Guidelines 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 9305e92..cb4bcc4 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,6 +1,16 @@ -> [!IMPORTANT] -> **Important:** We released an agent skill to help you implement email verification with the Digital Credentials API. Try out the skill from the [Android skills -> repository](https://github.com/android/skills). +## 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: + + android skills add --skill verified-email + +
+ +## 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 @@ -15,8 +25,8 @@ Manager: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -24,8 +34,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` 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 deleted file mode 100644 index 2134577..0000000 --- a/identity/verified-email/references/android/identity/digital-credentials/email-verification.md +++ /dev/null @@ -1,145 +0,0 @@ -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: - -- Android compatibility -- User experience -- Accounts supported -- Implication of email deliverability -- Comparison with [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) - -This guide assumes you are familiar with the following concepts: - -- [Credential Manager](https://developer.android.com/identity/credential-manager) -- [Digital Credentials](https://developer.android.com/identity/digital-credentials) -- [Verifiable Credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) - -## 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. - -## 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 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. - -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. - - > [!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. - - > [!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: - -- **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. - -#### 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. -![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. -![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. -![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. - -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). - -> [!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. -> For phone number verification, see [Verify phone numbers with digital credentials](https://developer.android.com/identity/digital-credentials/phone-number-verification). For a Firebase solution, see [Firebase Phone Number Verification](https://firebase.google.com/docs/phone-number-verification). Note that you can't verify a phone number and email in the same invocation as these are separate capabilities. - -## 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. - -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. - -### 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: - -- **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. -- **Platform support**: The Credential Manager flow is an Android-only solution. -- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It cannot be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes). - -## Next steps - -To implement this feature in your app, see the [Implementation guide](https://developer.android.com/identity/digital-credentials/email-verification-implementation). \ No newline at end of file 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 bfe19e5..089cc21 100644 --- a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -44,8 +44,8 @@ Add the following dependencies to your app module's `build.gradle` file: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -53,8 +53,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md index 566374a..d78cc17 100644 --- a/jetpack-compose/adaptive/SKILL.md +++ b/jetpack-compose/adaptive/SKILL.md @@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-02' + last-updated: '2026-07-30' keywords: - android - ui 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 8b00b01..b7b2763 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 @@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts. `lib.versions.toml`. [versions] - compose = "1.12.0-beta02" + compose = "1.12.0-rc01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 bb78a97..b4d7eeb 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 @@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co `lib.versions.toml`. [versions] - compose = "1.12.0-beta02" + compose = "1.12.0-rc01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 2812549..027793d 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-07-18' + last-updated: '2026-08-01' keywords: - R8 - proguard @@ -20,33 +20,40 @@ metadata: - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. - Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. -- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement - performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) +- **AGP** : If version is lower than 9.0, suggest migration to 9.0 for [build-time performance improvement](references/android/topic/performance/app-optimization/enable-app-optimization.md). - **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. ## Step 2. Analysis path selection - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and - `libs.versions.toml` to get the R8 version + `libs.versions.toml` to get the AGP and R8 versions. -- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. +- **If AGP \>= 9.3.0** : Proceed to **Path A (Standalone Task)**. -- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. +- **If AGP \< 9.3.0 and R8 \>= 9.3.7-dev** : Proceed to **Path B (Quantitative)**. -### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) +- If none of the conditions are met, proceed to **Path C (Heuristic)**. -- **Check requirements** : Python and `protobuf` package are mandatory. -- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. -- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. +### Path A: Standalone Gradle task (AGP \>= 9.3.0) -### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) +- **Step 1: Run standalone task** : Run `./gradlew :app:analyzeReleaseR8Config` to evaluate the R8 configuration. You MUST wait for this command to finish before proceeding. +- **Step 2: Convert to JSON** : The report is generated at `app/build/reports/r8/r8-config-analyzer-release.pb`. You MUST explicitly run the conversion script by executing: `python3 .agents/skills/r8-analyzer/scripts/convert_pb_to_json.py`. Wait for this command to finish. +- **Step 3: Analyze** : You MUST explicitly run the analysis script by executing: `python3 .agents/skills/r8-analyzer/scripts/analyze.py`. This outputs `tmp/keepradius/analysis_result.txt`. Wait for this command to finish. + +### Path B: Quantitative data generation (R8 \>= 9.3.7-dev and AGP \< 9.3.0) + +- **Step 1: Check requirements** : Python and `protobuf` package are mandatory. +- **Step 2: Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](https://developer.android.com/agents/skills/performance/r8-analyzer/references/CONFIGURATION-ANALYZER) to generate the proto file using R8 configuration analyzer, convert it to JSON and analyze the result. +- **Step 3: Analyze** : You MUST ensure the analysis produces `tmp/keepradius/analysis_result.txt` for scores and rule impact metrics. + +### Path C: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) *(Use ONLY if quantitative data generation is not possible)* -- **Manual evaluation** : Inspect `proguard-rules.pro`. -- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. -- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). -- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. +- **Step 1: Manual evaluation** : Inspect `proguard-rules.pro`. +- **Step 2: Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. +- **Step 3: Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](https://developer.android.com/agents/skills/performance/r8-analyzer/references/REFLECTION-GUIDE) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). +- **Step 4: Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. ## Step 3. Report generation diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md deleted file mode 100644 index 4039dcf..0000000 --- a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md +++ /dev/null @@ -1,287 +0,0 @@ -On each step, keep the user informed of the progress by displaying the output. - -### 1. Requirements - -- R8 Version: 9.3.7-dev or later - -### 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: - - 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: - - # Remove the intermediate JSON and the directory containing protobuf files - rm tmp/r8analysis/keepruleradius.json - rm tmp/r8analysis/*.pb - - # Copy the previous result to history before deleting the analysis - if [ -f tmp/r8analysis/analysis_result.txt ]; - then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && - rm tmp/r8analysis/analysis_result.txt; fi - -### 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 - - ./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. - - import sys - import os - import glob - from google.protobuf import json_format - import keep_radius_pb2 - - def convert_pb_to_json(input_pb_path, output_json_path): - bundle = keep_radius_pb2.BlastRadiusContainer() - - try: - with open(input_pb_path, "rb") as pb_file: - binary_data = pb_file.read() - except Exception as e: - print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) - return False - - try: - bundle.ParseFromString(binary_data) - except Exception as e: - print(f"Error parsing protobuf: {e}", file=sys.stderr) - return False - - try: - json_string = json_format.MessageToJson( - bundle, - always_print_fields_with_no_presence=True, - preserving_proto_field_name=True, - indent=4 - ) - with open(output_json_path, "w", encoding="utf-8") as json_file: - json_file.write(json_string) - return True - except Exception as e: - print(f"Error writing JSON: {e}", file=sys.stderr) - return False - - if __name__ == "__main__": - input_pb = sys.argv[1] if len(sys.argv) > 1 else None - if not input_pb: - pb_files = glob.glob("tmp/r8analysis/*.pb") - if not pb_files: - print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) - sys.exit(1) - input_pb = sorted(pb_files)[-1] # Use the most recent one - output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" - if not convert_pb_to_json(input_pb, output_json): - sys.exit(1) - -### 6. Analyze - -Run the following analysis script on the generated JSON to get the impact of the -keep rules and sort it. - - import json, sys - - def analyze(path): - try: - with open(path, 'r') as f: - d = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return - - # Build reference map - c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} - r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} - - tot_opt = tot_obf = tot_shr = tot_items = 0 - - # Tally constraints across all kept items - for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): - for i in d.get(tbl, []): - tot_items += 1 - kb = i.get('kept_by', []) - if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 - if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 - if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 - - # Find denominator - bi = d.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else tot_items - - # Check for globals - globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] - def score(cnt, flag): - if any(flag in src for src in globals_src): return 0.0 - return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) - - result = [ - f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", - f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", - f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" - ] - for line in result: - print(line) - with open("tmp/r8analysis/analysis_result.txt", "w") as f: - f.write("\n".join(result)) - - if __name__ == "__main__": - path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" - analyze(path) - -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. - - import json, sys - - def report(path): - try: - with open(path, 'r') as f: - data = json.load(f) - except Exception as e: - print(f"Error loading JSON: {e}") - return - - # Calculate denominator for percentage - bi = data.get('build_info', {}) - live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) - denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) - - processed = [] - for r in data.get('keep_rule_blast_radius_table', []): - br = r.get('blast_radius', {}) - c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) - impact = c + f + m - if impact == 0: - continue - impact_pct = (impact / denom * 100) if denom > 0 else 0.0 - processed.append({ - 'id': r.get('id'), - 'source': r.get('source'), - 'impact': impact, - 'impact_pct': f"{impact_pct:.2f}%", - 'classes': c, - 'fields': f, - 'methods': m, - 'subsumed_by': br.get('subsumed_by', []) - }) - - processed.sort(key=lambda x: x['impact'], reverse=True) - - # Output JSON for the agent to fetch and process - print(json.dumps({ - "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], - "subsumed": [r for r in processed if r['subsumed_by']] - }, indent=2)) - - if __name__ == "__main__": - report("tmp/r8analysis/keepruleradius.json") - -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 - -### 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 - - 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. - - from google.protobuf import descriptor as _descriptor - from google.protobuf import descriptor_pool as _descriptor_pool - from google.protobuf import runtime_version as _runtime_version - from google.protobuf import symbol_database as _symbol_database - from google.protobuf.internal import builder as _builder - _runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 4, - '', - 'keep_radius.proto' - ) - # @@protoc_insertion_point(imports) - - _sym_db = _symbol_database.Default() - DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') - - _globals = globals() - _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) - _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) - if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' - _globals['_KEEPRULETAG']._serialized_start = 3332 - _globals['_KEEPRULETAG']._serialized_end = 3363 - _globals['_KEEPCONSTRAINT']._serialized_start = 3365 - _globals['_KEEPCONSTRAINT']._serialized_end = 3437 - _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 - _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 - _globals['_BLASTRADIUS']._serialized_start = 351 - _globals['_BLASTRADIUS']._serialized_end = 470 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 - _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 - _globals['_KEEPCONSTRAINTS']._serialized_start = 601 - _globals['_KEEPCONSTRAINTS']._serialized_end = 707 - _globals['_KEPTCLASSINFO']._serialized_start = 709 - _globals['_KEPTCLASSINFO']._serialized_end = 805 - _globals['_KEPTFIELDINFO']._serialized_start = 807 - _globals['_KEPTFIELDINFO']._serialized_end = 903 - _globals['_KEPTMETHODINFO']._serialized_start = 905 - _globals['_KEPTMETHODINFO']._serialized_end = 1003 - _globals['_FIELDREFERENCE']._serialized_start = 1005 - _globals['_FIELDREFERENCE']._serialized_end = 1102 - _globals['_METHODREFERENCE']._serialized_start = 1104 - _globals['_METHODREFERENCE']._serialized_end = 1203 - _globals['_PROTOREFERENCE']._serialized_start = 1205 - _globals['_PROTOREFERENCE']._serialized_end = 1280 - _globals['_TYPEREFERENCE']._serialized_start = 1282 - _globals['_TYPEREFERENCE']._serialized_end = 1334 - _globals['_TYPEREFERENCELIST']._serialized_start = 1336 - _globals['_TYPEREFERENCELIST']._serialized_end = 1395 - _globals['_FILEORIGIN']._serialized_start = 1398 - _globals['_FILEORIGIN']._serialized_end = 1557 - _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 - _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 - _globals['_TEXTFILEORIGIN']._serialized_start = 1634 - _globals['_TEXTFILEORIGIN']._serialized_end = 1718 - _globals['_MAVENCOORDINATE']._serialized_start = 1720 - _globals['_MAVENCOORDINATE']._serialized_end = 1805 - _globals['_BUILDINFO']._serialized_start = 1808 - _globals['_BUILDINFO']._serialized_end = 1962 - _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 - _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 - # @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/performance/r8-analyzer/references/REFLECTION-GUIDE.md b/performance/r8-analyzer/references/REFLECTION-GUIDE.md deleted file mode 100644 index 8f23679..0000000 --- a/performance/r8-analyzer/references/REFLECTION-GUIDE.md +++ /dev/null @@ -1,139 +0,0 @@ -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. - -- **Example Code:** - `kotlin - val taskClass = Class.forName(className) - val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask` - -- **Suggested Keep Rule:** - \`\`\`proguard - - -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)` - -- **Suggested Keep Rule:** - \`\`\`proguard - - # Keep the class itself (R8 usually handles this, but explicit rules ensure stability) - - -keep class com.example.app.MyService { - (); } \`\`\` - -### 3. Annotation-Based Reflection (Methods/Classes) - -**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` - -- **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 \*; -} -\`\`\` - -### 4. Optional Dependencies (Soft Dependencies) - -**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 \*/ } -\`\`\` - -- **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. - -- **Look for:** `getDeclaredField("...")` or `getDeclaredMethod("...")` followed by `isAccessible = true`. -- **Example Code:** - `kotlin - val secretField = instance::class.java.getDeclaredField("secretMessage") - secretField.isAccessible = true` - -- **Suggested Keep Rule:** - \`\`\`proguard - - # Specifically keep the private field/method by name and type - - -keepclassmembers class com.example.LibraryClass { - private java.lang.String secretMessage; - } - \`\`\` - -### 6. Parcelable (Manual Implementation) - -**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 - }` - -- **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. - -- **Look for:** Unnecessary generic Enum keep rules in ProGuard files. -- **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 diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 0493cd7..d17590e 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-07-18' + last-updated: '2026-07-31' keywords: - android - engage @@ -29,7 +29,7 @@ Follow these steps to assist the developer: - Check if the integration is for TV or mobile. If the integration is for TV, read the TV-specific sections in [patterns.md](references/patterns.md) as well. - Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. For example, `com.google.android.engage.food.service.AppEngageFoodClient`. - **Note:** Initializing the client class requires a `Context` parameter. For example, `AppEngageFoodClient(context)`. - - Always refer to [common.md](references/common.md) for common entities. + - Always refer to [common.md](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/common) for common entities. - Ask which cluster type they want to publish from the supported cluster types for that vertical. - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. @@ -69,7 +69,7 @@ Follow these steps to assist the developer: - 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 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](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/common). - 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 @@ -102,7 +102,7 @@ Follow these steps to assist the developer: - [Listen Vertical](references/android/guide/playcore/engage/listen.md) - [Read Vertical](references/android/guide/playcore/engage/read.md) - [Shopping Vertical](references/android/guide/playcore/engage/shopping.md) - - [Social Vertical](https://developer.android.com/guide/playcore/engage/social) + - [Social Vertical](references/android/guide/playcore/engage/social.md) - [Travel Vertical](references/android/guide/playcore/engage/travel.md) - [Health and Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) - [Other Verticals](references/android/guide/playcore/engage/otherverticals.md) 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 new file mode 100644 index 0000000..b1e8f78 --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md @@ -0,0 +1,702 @@ +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. + +## Integration detail + +The following section captures the integration detail. + +### Terminology + +***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. + +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. + +- Post + + - Image in portrait mode and Timestamp, or + - Image in portrait mode + text content and Timestamp +- Profile + + - Avatar, Name or Handle, Additional image +- Interactions + + - Count and label only, or + - Count and visual (icon) + +**SocialPostEntity** contains profile, post and interaction related metadata. + +- Profile + + - Avatar, Name or Handle, additional text, additional image +- Post + + - Text and Timestamp, or + - Rich media (image or rich URL) and Timestamp, or + - Text and rich media (image or rich URL) and Timestamp, or + - Video preview (thumbnail and duration) and Timestamp +- Interactions + + - Count \& label only, or + - Count \& visual (icon) + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.6.0' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 (`PortraitMediaEntity`, or `SocialPostEntity`) | At most 50 (`PortraitMediaEntity`, or `SocialPostEntity`) | + +### 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: + +1. `PortraitMediaEntity` +2. `SocialPostEntity` + +The charts below outline available attributes and requirements for each type. + +#### `PortraitMediaEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** for all surfaces other than Google TV | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** |||| +| Image(s) | Required | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| Is video content | Optional | Is the post a video? | boolean | +| Video duration | Optional | The duration of the video in milliseconds. | Long | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel" | String(recommended max 25 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge. for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Optional | Indicate the number of interactions, for example - "3.7 M.". **Note:** If both Count and Count Value are provided, Count will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Optional | Indicate the number of interactions with an optional label, for example - "3.7 M Likes.". **Note:** If both CountWithOptionalLabel and Count Value are provided, one of them will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Count Value | Optional | The number of interactions as a value. **Note:** Provide Count Value instead of Count if your app doesn't handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count is used. | Long | +| Label | Optional | Indicate what the interaction label is for. For example - "Likes". | String | +| Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `SocialPostEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback URIs | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** At least one of TextContent, Image or WebContent is required |||| +| Image(s) | Optional | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| **Video Content (Optional)** |||| +| Duration | Required | The duration of the video in milliseconds. | Long | +| Image | Required | Preview image of the video content. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Link Preview (Optional)** |||| +| Link Preview - Title | Required | Text to indicate the title of the web page content | String | +| Link Preview - Hostname | Required | Text to indicate the web page owner, eg "INSIDER" | String | +| Link Preview - Image | Optional | Hero image for the web content | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel." | String(recommended max 25 chars) | +| Additional Text | Optional | Could be used as profile id or handle or additional metadata For example "@John-Doe", "5M followers", "You might like", "Trending", "5 new posts" | String(recommended max 40 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge, for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Required | Indicate the number of interactions, for example - "3.7 M." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Required | Indicate the number of interactions with an optional label, for example - "3.7 M Likes." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Label | Optional If not provided, **Visual** must be provided. | Indicate what the interaction is for. For example - "Likes." | String (recommended max 20 chars for count + label combined) | +| Visual | Optional If not provided, **Label** must be provided. | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### Image specifications + +The images are required to be hosted on public CDNs so that Google can access +them. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +### 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) + +`AppEngageSocialClient` is responsible for publishing social clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +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) + +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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) + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list `RecommendationCluster` objects. + +A `RecommendationCluster` object can have the following attributes: + +| Attribute | Requirement | Description | +|---|---|---| +| List of SocialPostEntity, or PortraitMediaEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | +| Title | **Required** | The title for the Recommendation Cluster (for example, *Latest from your friends*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Subtitle | Optional | The subtitle for the Recommendation Cluster. | +| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | + +> [!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. + +> [!IMPORTANT] +> **Important:** For social apps, it's critical to update recommendations 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an +error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION + // broadcast is received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + +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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation**, clusters will be published and visible to users. \ 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 49d8975..c0c0058 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 @@ -48,12 +48,18 @@ Before you begin, complete the following steps: 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 in - the `AndroidManifest.xml` file. +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 ``. - + + + + + > [!IMPORTANT] > **Important:** The Engage service environment declaration is only needed for your release build. For your local testing with the verification app, don't include this element. diff --git a/play/engage-sdk-integration/references/common.md b/play/engage-sdk-integration/references/common.md deleted file mode 100644 index 52c3d5e..0000000 --- a/play/engage-sdk-integration/references/common.md +++ /dev/null @@ -1,519 +0,0 @@ -This file defines common data models and entities used across the Engage SDK. - - { - "entities": { - "Image": { - "package": "com.google.android.engage.common.datamodel.Image", - "fields": { - "imageUri": { - "type": "Uri", - "requirement": "Required", - "setter": "setImageUri(Uri)", - "getter": "getImageUri()" - }, - "height": { - "type": "Integer", - "requirement": "Optional" - }, - "width": { - "type": "Integer", - "requirement": "Optional" - }, - "accessibilityText": { - "type": "String", - "requirement": "Required", - "setter": "setAccessibilityText(String)", - "getter": "getAccessibilityText()" - }, - "theme": { - "type": "ImageThemeEnum", - "requirement": "Optional" - }, - "cropType": { - "type": "ImageCropTypeEnum", - "requirement": "Optional" - }, - "imageHeightInPixel": { - "requirement": "Required", - "setter": "setImageHeightInPixel(int)", - "type": "Integer", - "getter": "getImageHeightInPixel()" - }, - "imageWidthInPixel": { - "requirement": "Required", - "setter": "setImageWidthInPixel(int)", - "type": "Integer", - "getter": "getImageWidthInPixel()" - }, - "imageTheme": { - "requirement": "Optional", - "setter": "setImageTheme(@ImageTheme int)", - "type": "@ImageTheme int", - "getter": "getImageTheme()" - }, - "imageCropType": { - "requirement": "Optional", - "setter": "setImageCropType(@ImageCropType int)", - "type": "@ImageCropType int", - "getter": "getImageCropType()" - } - } - }, - "Price": { - "package": "com.google.android.engage.common.datamodel.Price", - "fields": { - "currentPrice": { - "type": "String", - "requirement": "Required", - "setter": "setCurrentPrice(String)", - "getter": "getCurrentPrice()" - }, - "strikethroughPrice": { - "type": "String", - "requirement": "Optional", - "setter": "setStrikethroughPrice(String)", - "getter": "getStrikethroughPrice()" - } - } - }, - "Rating": { - "package": "com.google.android.engage.common.datamodel.Rating", - "fields": { - "ratingValue": { - "type": "Float", - "requirement": "Required" - }, - "maxValue": { - "type": "Double", - "requirement": "Required", - "setter": "setMaxValue(double)", - "getter": "getMaxValue()" - }, - "ratingCount": { - "type": "Integer", - "requirement": "Optional" - }, - "ratingCountValue": { - "type": "String", - "requirement": "Optional" - }, - "currentValue": { - "requirement": "Required", - "setter": "setCurrentValue(double)", - "type": "Double", - "getter": "getCurrentValue()" - }, - "count": { - "requirement": "Optional", - "setter": "setCount(String)", - "type": "String", - "getter": "getCount()" - }, - "countValue": { - "requirement": "Optional", - "setter": "setCountValue(long)", - "type": "Long", - "getter": "getCountValue()" - } - } - }, - "DisplayTimeWindow": { - "package": "com.google.android.engage.common.datamodel.DisplayTimeWindow", - "fields": { - "startTimeMillis": { - "type": "Long", - "requirement": "Required" - }, - "endTimeMillis": { - "type": "Long", - "requirement": "Required" - }, - "startTimestampMillis": { - "requirement": "Optional", - "setter": "setStartTimestampMillis(long)", - "type": "Long", - "getter": "getStartTimestampMillis()" - }, - "endTimestampMillis": { - "requirement": "Optional", - "setter": "setEndTimestampMillis(long)", - "type": "Long", - "getter": "getEndTimestampMillis()" - } - } - }, - "AccountProfile": { - "package": "com.google.android.engage.common.datamodel.AccountProfile", - "fields": { - "accountId": { - "type": "@NonNull String", - "requirement": "Required", - "setter": "setAccountId(@NonNull String)", - "getter": "getAccountId()" - }, - "profileId": { - "type": "@NonNull String", - "requirement": "Optional", - "setter": "setProfileId(@NonNull String)", - "getter": "getProfileId()" - }, - "accountName": { - "type": "String", - "requirement": "Optional" - }, - "profileImage": { - "type": "Image", - "requirement": "Optional" - }, - "locale": { - "requirement": "Optional", - "setter": "setLocale(@NonNull String)", - "type": "@NonNull String", - "getter": "getLocale()" - } - } - }, - "Address": { - "package": "com.google.android.engage.common.datamodel.Address", - "fields": { - "city": { - "type": "@NonNull String", - "requirement": "Required", - "setter": "setCity(@NonNull String)", - "getter": "getCity()" - }, - "country": { - "type": "@NonNull String", - "requirement": "Required", - "setter": "setCountry(@NonNull String)", - "getter": "getCountry()" - }, - "displayAddress": { - "type": "@NonNull String", - "requirement": "Required", - "setter": "setDisplayAddress(@NonNull String)", - "getter": "getDisplayAddress()" - }, - "streetAddress": { - "type": "String", - "requirement": "Optional", - "setter": "setStreetAddress(String)", - "getter": "getStreetAddress()" - }, - "state": { - "type": "String", - "requirement": "Optional", - "setter": "setState(String)", - "getter": "getState()" - }, - "zipCode": { - "type": "String", - "requirement": "Optional", - "setter": "setZipCode(String)", - "getter": "getZipCode()" - }, - "neighborhood": { - "type": "String", - "requirement": "Optional", - "setter": "setNeighborhood(String)", - "getter": "getNeighborhood()" - } - } - }, - "Badge": { - "package": "com.google.android.engage.common.datamodel.Badge", - "fields": { - "text": { - "type": "String", - "requirement": "Optional", - "setter": "setText(String)", - "getter": "getText()" - }, - "image": { - "type": "Image", - "requirement": "Optional", - "setter": "setImage(Image)", - "getter": "getImage()" - } - } - }, - "LocalizedTimestamp": { - "package": "com.google.android.engage.common.datamodel.LocalizedTimestamp", - "fields": { - "timestamp": { - "type": "Instant", - "requirement": "Required", - "setter": "setTimestamp(Instant)", - "getter": "getTimestamp()" - }, - "timezone": { - "type": "DateTimeZone", - "requirement": "Required", - "setter": "setTimezone(DateTimeZone)", - "getter": "getTimezone()" - } - } - }, - "SignInCardEntity": { - "package": "com.google.android.engage.common.datamodel.SignInCardEntity", - "fields": { - "actionText": { - "type": "String", - "requirement": "Required", - "setter": "setActionText(String)", - "getter": "getActionText()" - }, - "actionUri": { - "type": "Uri", - "requirement": "Required", - "setter": "setActionUri(Uri)", - "getter": "getActionUri()" - }, - "title": { - "type": "String", - "requirement": "Optional", - "setter": "setTitle(String)", - "getter": "getTitle()" - }, - "subtitle": { - "type": "String", - "requirement": "Optional", - "setter": "setSubtitle(String)", - "getter": "getSubtitle()" - }, - "entityId": { - "type": "String", - "requirement": "Optional", - "setter": "setEntityId(String)", - "getter": "getEntityId()" - }, - "posterImages": { - "type": "List", - "requirement": "Required", - "adder": "addPosterImage(Image)", - "getter": "getPosterImages()", - "adderAll": "addPosterImages(List)" - } - } - }, - "GenericFeaturedEntity": { - "package": "com.google.android.engage.common.datamodel.GenericFeaturedEntity", - "fields": { - "entityId": { - "type": "String", - "requirement": "Optional", - "setter": "setEntityId(String)", - "getter": "getEntityId()" - }, - "posterImages": { - "type": "List", - "requirement": "Required", - "adder": "addPosterImage(Image)", - "getter": "getPosterImages()", - "adderAll": "addPosterImages(List)" - }, - "actionUri": { - "type": "Uri", - "requirement": "Required", - "setter": "setActionUri(Uri)", - "getter": "getActionUri()" - }, - "title": { - "type": "String", - "requirement": "Optional", - "setter": "setTitle(String)", - "getter": "getTitle()" - }, - "description": { - "type": "String", - "requirement": "Optional", - "setter": "setDescription(String)", - "getter": "getDescription()" - }, - "subtitles": { - "type": "List", - "requirement": "Optional", - "adder": "addSubtitle(String)", - "adderAll": "addSubtitles(List)" - }, - "badges": { - "type": "List", - "requirement": "Optional", - "adder": "addBadge(Badge)", - "adderAll": "addBadges(List)" - }, - "contentCategories": { - "type": "List<@EligibleContentCategory int>", - "requirement": "Optional", - "adder": "addContentCategory(@EligibleContentCategory int)", - "adderAll": "addContentCategories(List)" - }, - "displayTimeWindows": { - "type": "List", - "requirement": "Optional", - "adder": "addDisplayTimeWindow(DisplayTimeWindow)", - "getter": "getDisplayTimeWindows()" - }, - "allDisplayTimeWindows": { - "type": "List>", - "requirement": "Optional", - "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", - "adderAll": "addAllDisplayTimeWindow(List)" - } - } - }, - "SubscriptionEntity": { - "package": "com.google.android.engage.common.datamodel.SubscriptionEntity", - "fields": {} - }, - "UserSettingsCardEntity": { - "package": "com.google.android.engage.common.datamodel.UserSettingsCardEntity", - "fields": { - "actionText": { - "type": "String", - "requirement": "Required", - "setter": "setActionText(String)", - "getter": "getActionText()" - }, - "actionUri": { - "type": "Uri", - "requirement": "Required", - "setter": "setActionUri(Uri)", - "getter": "getActionUri()" - }, - "title": { - "type": "String", - "requirement": "Optional", - "setter": "setTitle(String)", - "getter": "getTitle()" - }, - "subtitle": { - "type": "String", - "requirement": "Optional", - "setter": "setSubtitle(String)", - "getter": "getSubtitle()" - }, - "entityId": { - "type": "String", - "requirement": "Optional", - "setter": "setEntityId(String)", - "getter": "getEntityId()" - }, - "posterImages": { - "type": "List", - "requirement": "Required", - "adder": "addPosterImage(Image)", - "getter": "getPosterImages()", - "adderAll": "addPosterImages(List)" - } - } - }, - "ArticleEntity": { - "package": "com.google.android.engage.common.datamodel.ArticleEntity", - "fields": { - "entityId": { - "type": "String", - "requirement": "Optional", - "setter": "setEntityId(String)", - "getter": "getEntityId()" - }, - "posterImages": { - "type": "List", - "requirement": "Required", - "adder": "addPosterImage(Image)", - "getter": "getPosterImages()", - "adderAll": "addPosterImages(List)" - }, - "actionUri": { - "type": "Uri", - "requirement": "Required", - "setter": "setActionUri(Uri)", - "getter": "getActionUri()" - }, - "title": { - "type": "String", - "requirement": "Optional", - "setter": "setTitle(String)", - "getter": "getTitle()" - }, - "description": { - "type": "String", - "requirement": "Optional", - "setter": "setDescription(String)", - "getter": "getDescription()" - }, - "subtitles": { - "type": "List", - "requirement": "Optional", - "adder": "addSubtitle(String)", - "adderAll": "addSubtitles(List)" - }, - "badges": { - "type": "List", - "requirement": "Optional", - "adder": "addBadge(Badge)", - "adderAll": "addBadges(List)" - }, - "contentCategories": { - "type": "List<@EligibleContentCategory int>", - "requirement": "Optional", - "adder": "addContentCategory(@EligibleContentCategory int)", - "adderAll": "addContentCategories(List)" - }, - "progressPercentage": { - "type": "Integer", - "requirement": "Required", - "setter": "setProgressPercentage(int)", - "getter": "getProgressPercentage()", - "requiredFor": [ - "ContinuationCluster" - ] - }, - "lastEngagementTimestampMillis": { - "type": "Long", - "requirement": "Required", - "setter": "setLastEngagementTimestampMillis(long)", - "getter": "getLastEngagementTimestampMillis()", - "requiredFor": [ - "ContinuationCluster" - ] - }, - "source": { - "type": "Badge", - "requirement": "Optional", - "setter": "setSource(Badge)", - "getter": "getSource()" - }, - "lastContentPublishTimestampMillis": { - "type": "Long", - "requirement": "Optional", - "setter": "setLastContentPublishTimestampMillis(Long)", - "getter": "getLastContentPublishTimestampMillis()" - }, - "displayTimeWindows": { - "type": "List", - "requirement": "Optional", - "adder": "addDisplayTimeWindow(DisplayTimeWindow)", - "getter": "getDisplayTimeWindows()" - }, - "allDisplayTimeWindows": { - "type": "List>", - "requirement": "Optional", - "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", - "adderAll": "addAllDisplayTimeWindow(List)" - } - } - } - }, - "intents": { - "ACTION_PUBLISH_RECOMMENDATION": "com.google.android.engage.action.PUBLISH_RECOMMENDATION", - "ACTION_PUBLISH_FEATURED": "com.google.android.engage.action.PUBLISH_FEATURED", - "ACTION_PUBLISH_CONTINUATION": "com.google.android.engage.action.PUBLISH_CONTINUATION" - }, - "imports": [ - "com.google.android.engage.service.AppEngagePublishClient", - "com.google.android.engage.service.AppEngageErrorCode", - "com.google.android.engage.service.AppEngageException", - "com.google.android.engage.service.AppEngagePublishStatusCode" - ] - } \ 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 deleted file mode 100644 index 6784606..0000000 --- a/play/play-billing-library-version-upgrade/SKILL.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: play-billing-library-version-upgrade -description: Use this skill when upgrading or migrating an Android project from any - legacy Google Play Billing Library (PBL) version to the latest stable version of - PBL. -license: Complete terms in LICENSE.txt -metadata: - author: Google LLC - last-updated: '2026-07-02' - keywords: - - android - - play billing - - play billing library - - pbl - - upgrade - - migration - - deprecation - - google play ---- - -## Phase 0: Intent Message - -**Reporting Action**: Before proceeding, immediately tell the user: "I will -upgrade Play Billing Library to the latest version." - -## Phase 1: Discovery \& Situational Awareness - -1. **Primary Check (Build Version)** : Locate the project's billing dependency (e.g., `com.android.billingclient:billing`) in `build.gradle`, `build.gradle.kts`, or `libs.versions.toml`. -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. - - **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. -- **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: - -- **[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). -- **Developer Documentation**: Consult your knowledge of the Google Play Billing documentation regarding the relevant features used in this app (e.g., Subscriptions, One-Time Products). -- **Develop the Plan**: Identify every specific code change required (API removals, class replacements, logic shifts) and print this out as a checklist. - -## 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.* - -### Step 1: SDK \& Environment Alignment - -- **Action** : Update `build.gradle` to meet SDK requirements (e.g., "PBL 9 requires `compileSdk` 35"). -- **Gradle Version**: Verify if the new library requires a newer Android Gradle Plugin (AGP) or Kotlin version. - -### Step 2: Intent-based Refactoring - -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. - -### Step 3: Sequential Verification (Only applicable for Stepped Migrations) - -1. **Upgrade** to the first major intermediate version in your path. -2. **Run `./gradlew assembleDebug`** to verify no intermediate breaking changes were missed. -3. **Repeat** until you reach the final target version. - -### Step 4: Final Validation Checklist - -1. **Smart Checklist Verification**: -2. Open [references/version-checklist.md](references/version-checklist.md) and locate the **Smart Version-Specific Checklist**. -3. **Action**: For every version between your \[Detected Effective Version\] and \[Detected New Version\], verify that every item has been addressed in the code using "Find in Files" or structural analysis. -4. **Tests** : Run all unit and implementation tests (`./gradlew test`). -5. **Clean Build** : Verify the project completes a full clean build: `./gradlew clean assembleDebug`. Then, run `./gradlew sync` and `./gradlew build` so that the user can immediately test the new version manually. - -## Final Report - -Explain the "Why" to the developer: - -- "I updated your SDK to \[Version\] because PBL \[Version\] requires it for \[Reason from docs\]." -- "I removed your custom `retryConnection()` logic because it is now handled natively by the library using `enableAutoServiceReconnection()`." -- "Successfully upgraded from PBL \[Old\] to PBL \[New\] and verified with unit tests. Based on an analysis of features in the latest library and this application's current feature set, I suggest exploring \[New Feature\] (e.g., Prepaid Plans or Installments) from the latest release because it is now available but not yet implemented." 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 deleted file mode 100644 index 462f373..0000000 --- a/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md +++ /dev/null @@ -1,1226 +0,0 @@ -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. - -## Google Play Billing Library 9.1.0 Release (2026-06-18) - -Version 9.1.0 of the Google Play Billing Library and Kotlin extensions are now available. - -### Summary of changes - -- New APIs for [Billing Choice](https://developer.android.com/google/play/billing/billingchoice): - - Added [`BillingClient.getBillingChoiceInfoAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getBillingChoiceInfoAsync(com.android.billingclient.api.GetBillingChoiceInfoParams,com.android.billingclient.api.BillingChoiceInfoResponseListener)) to retrieve information about billing choices available to the user. - - Added [`BillingChoiceInfo`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfo) class which contains the response details, including image URLs and loyalty information. - - Added [`GetBillingChoiceInfoParams`](https://developer.android.com/reference/com/android/billingclient/api/GetBillingChoiceInfoParams) to configure the request. - - Added [`BillingChoiceInfoResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfoResponseListener) to receive the callback. - - Added Kotlin extension [`BillingClient.getBillingChoiceInfo()`](https://developer.android.com/reference/kotlin/com/android/billingclient/api/package-summary#(com.android.billingclient.api.BillingClient).getBillingChoiceInfo(com.android.billingclient.api.GetBillingChoiceInfoParams)) suspend function. - - Added [`BillingClient.showBillingProgramInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showBillingProgramInformationDialog(android.app.Activity,com.android.billingclient.api.BillingProgramInformationDialogParams,com.android.billingclient.api.BillingProgramInformationDialogListener)) to show an information dialog for a billing program. - - Added [`BillingProgramInformationDialogParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogParams) to configure the dialog. - - Added [`BillingProgramInformationDialogListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogListener) to receive the callback. - - Added [`BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails) to provide details about billing choice availability. - - Added [`ChoiceScreenType`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType) to specify the type of choice screen. - -## 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. - -### 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. - - > [!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. - - 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. - -## Google Play Billing Library 8.3.0 Release (2025-12-23) - -Version 8.3.0 of the Google Play Billing Library and Kotlin extensions are now available. - -### Summary of changes - -- New APIs for [external payments](https://developer.android.com/google/play/billing/externalpaymentlinks): - - - Added classes to support external payments flow: - - [`BillingProgram.EXTERNAL_PAYMENTS`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingProgram#EXTERNAL_PAYMENTS) - - [`EnableBillingProgramParams`](https://developer.android.com/reference/com/android/billingclient/api/EnableBillingProgramParams) - - [`DeveloperBillingOptionParams`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperBillingOptionParams) - - [`DeveloperProvidedBillingDetails`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails) - - [`DeveloperProvidedBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingListener) - - Added [`enableBillingProgram(EnableBillingProgramParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableBillingProgram(com.android.billingclient.api.EnableBillingProgramParams)) to enable external payments. - - Added [`BillingFlowParams.Builder.enableDeveloperBillingOption`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#enableDeveloperBillingOption(com.android.billingclient.api.DeveloperBillingOptionParams)) to launch the external payments flow. - -## 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. - -### Bug fixes - -- Fixed a bug in [`isBillingProgramAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isBillingProgramAvailableAsync(int,com.android.billingclient.api.BillingProgramAvailabilityListener)) and [`createBillingProgramReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createBillingProgramReportingDetailsAsync(com.android.billingclient.api.BillingProgramReportingDetailsParams,com.android.billingclient.api.BillingProgramReportingDetailsListener)). Update to version 8.2.1 to use these APIs introduced in 8.2.0. - -## Google Play Billing Library 8.2.0 Release (2025-12-09) - -Version 8.2.0 of the Google Play Billing Library and Kotlin extensions are now available. - -### Summary of changes - -- New APIs for [external content links](https://developer.android.com/google/play/billing/externalcontentlinks) and [external offers](https://developer.android.com/google/play/billing/external): - - - Added [`enableBillingProgram`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableBillingProgram(int)) to setup the `BillingClient` for these programs. - - Added [`isBillingProgramAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isBillingProgramAvailableAsync(int,com.android.billingclient.api.BillingProgramAvailabilityListener)) to determine user eligibility. - - Added [`createBillingProgramReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createBillingProgramReportingDetailsAsync(com.android.billingclient.api.BillingProgramReportingDetailsParams,com.android.billingclient.api.BillingProgramReportingDetailsListener)) to create the external transaction token that must be used for reporting. - - Added [`launchExternalLink`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchExternalLink(android.app.Activity,com.android.billingclient.api.LaunchExternalLinkParams,com.android.billingclient.api.LaunchExternalLinkResponseListener)) to initiate the external link to a digital content offer or an app download. -- Changes to the [external offers program](https://developer.android.com/google/play/billing/external): - - - There are policy changes for the external offers program. See [program changes](https://support.google.com/googleplay/android-developer/answer/16505463) for details. To understand how to launch external offer flows with the new APIs, see the [integration guide](https://developer.android.com/google/play/billing/external/integration). - - Deprecated the [`BillingClient.Builder.enableExternalOffer`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableExternalOffer()) API. - - Deprecated the [`isExternalOfferAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isExternalOfferAvailableAsync(com.android.billingclient.api.ExternalOfferAvailabilityListener)) API. - - Deprecated the [`createExternalOfferReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createExternalOfferReportingDetailsAsync(com.android.billingclient.api.ExternalOfferReportingDetailsListener)) API. - - Deprecated the [`showExternalOfferInformationDialog`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showExternalOfferInformationDialog(android.app.Activity,com.android.billingclient.api.ExternalOfferInformationDialogListener)) API. - -## Google Play Billing Library 8.1.0 Release (2025-11-06) - -Version 8.1.0 of the Google Play Billing Library and Kotlin extensions are now available. - -### Summary of changes - -- 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. - - 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 `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. - -- 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. -- 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. - -### Summary of changes - -- *In-app items* will now be referred to as *one-time products*. - -- 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. -- 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). -- 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: - - `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. -- 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. -- 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())`. -- 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)). - -## 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. - -### 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). - -## 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. - -### 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). - -## 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. - -### 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. - - - 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). - - 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). - - - 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)). - - - 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)). - - - 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: - - - 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`. - -- 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. - -### Summary of changes - -- Fixed a bug in [`BillingClient.showAlternativeBillingOnlyInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showAlternativeBillingOnlyInformationDialog(android.app.Activity,%20com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener)) where the [`AlternativeBillingOnlyInformationDialogListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingOnlyInformationDialogListener) may not be called in certain cases when the dialog completes. - -## 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. - -### Summary of changes - -- Added APIs to support [external offers](https://developer.android.com/google/play/billing/external) - - Added [`BillingClient.Builder.enableExternalOffer()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableExternalOffer()) to enable the ability to provide external offers. - - Added [`BillingClient.isExternalOfferAvailableAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isExternalOfferAvailableAsync(com.android.billingclient.api.ExternalOfferAvailabilityListener)) to check the availability of providing external offers functionality. - - Added [`BillingClient.showExternalOfferInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showExternalOfferInformationDialog(android.app.Activity,%20com.android.billingclient.api.ExternalOfferInformationDialogListener)) to show an information dialog to users before leading users outside the app. - - Added [`BillingClient.createExternalOfferReportingDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createExternalOfferReportingDetailsAsync(com.android.billingclient.api.ExternalOfferReportingDetailsListener)) to create a payload required to report transactions made through external offers. - -## 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. - -### Summary of changes - -- Added APIs to support [alternative billing only (i.e. without user choice)](https://developer.android.com/google/play/billing/alternative) - - Added [`BillingClient.Builder.enableAlternativeBillingOnly()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBillingOnly()) to functionally enable the ability to offer alternative billing only. - - Added [`BillingClient.isAlternativeBillingOnlyAvailableAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isAlternativeBillingOnlyAvailableAsync(com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener)) to check the availability of offering alternative billing only. - - Added [`BillingClient.showAlternativeBillingOnlyInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showAlternativeBillingOnlyInformationDialog(android.app.Activity,%20com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener)) to show an information dialog to inform users when alternative billing only is being used. - - Added [`BillingClient.createAlternativeBillingOnlyReportingDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(com.android.billingclient.api.AlternativeBillingOnlyReportingDetailsListener)) to create a payload required to report transactions made through alternative billing only. -- Updated the user choice billing APIs - - Added [`UserChoiceBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceBillingListener) to replace [AlternativeBillingListener](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener) which has been marked as deprecated. - - Added [`UserChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails) to replace [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails) which has been marked as deprecated. - - Added [`BillingClient.Builder.enableUserChoiceBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableUserChoiceBilling(com.android.billingclient.api.UserChoiceBillingListener)) to replace [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)) which has been marked as deprecated. -- Added [`BillingClient.getBillingConfigAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getBillingConfigAsync(com.android.billingclient.api.GetBillingConfigParams,%20com.android.billingclient.api.BillingConfigResponseListener)) to retrieve Google Play country. - -## 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. - -### Summary of changes - -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. - -### 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). - - 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. -- 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). -- 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). - - 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). - - 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. - -## 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. - -### Summary of changes - -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. - -### Summary of changes - -- Added classes to support alternative billing flows on mobile/tablet for users in South Korea: - - [`AlternativeBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener) - - [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails) - - [`AlternativeChoiceDetails.Product`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails.Product) -- Added [`BillingFlowParams.SubscriptionUpdateParams.Builder.setOriginalExternalTransactionId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOriginalExternalTransactionId(java.lang.String)) method to specify the external transaction id of the originating subscription. -- Added [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)) method to allow users in South Korea to select an alternative billing option. - -## 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. - -This version contains the following changes. - -### Summary of changes - -- Added [`ProductDetails.SubscriptionOfferDetails.getOfferId()`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails#getOfferId()) method to retrieve the offer ID. -- Added [`ProductDetails.SubscriptionOfferDetails.getBasePlanId()`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails#getBasePlanId()) method to retrieve the base plan ID. -- Updated the `targetSdkVersion` to 31. - -## 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. - -This version contains the following changes. - -### Summary of 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). -- 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). -- 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. - -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). - -## 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. - -### 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 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. - -- 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 - [`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. - -- 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. - -### Java and Kotlin Bug fixes - -- Fix memory leak when [`endConnection()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#endConnection()) is called. -- Fix issue when the Google Play Billing Library is used by apps which use the single task launch mode. A [`onPurchasesUpdated()`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener#onpurchasesupdated) callback will be triggered when an app is resumed from the Android launcher and the billing dialog was visible prior to being suspended. - -### Unity Bug fixes - -- Update to Java version 3.0.3 to fix memory leak and resolve issue preventing purchases when an app is resumed from the Android launcher and the billing dialog was visible prior to being suspended. - -## 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. - -### Bug fixes - -- Fixed a bug in the Kotlin extension where the coroutine fails with error "Already resumed". -- Fixed unresolved references when the Kotlin extension is used with the kotlinx.coroutines library version 1.4+. - -## 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. - -### Bug fixes - -- Fixed a bug where if the app was killed and restored during the billing flow, `PurchasesUpdatedListener` may not be called with the purchase result. - -## 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. - -### Summary of changes - -- Removed rewarded SKU support. -- Removed the `ChildDirected` and `UnderAgeOfConsent` parameters. -- Removed deprecated developer payload methods. -- Removed deprecated methods `BillingFlowParams.setAccountId()` and `BillingFlowParams.setDeveloperId()`. -- Removed deprecated methods `BillingFlowParams.setOldSkus(String oldSku)` and `BillingFlowParams.addOldSku(String oldSku)`. -- Added nullability annotations. - -### Bug fixes - -- [`SkuDetails.getIntroductoryPriceCycles()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getIntroductoryPriceCycles()) now returns `int` instead of `String`. -- Fixed a bug where the billing flow would be treated as having extra params even if no extra params were set. - -## Google Play Billing Library 2.2.1 Release (2020-05-20) - -Version 2.2.1 of the Google Play Billing library is now available. - -### Bug fixes - -- Updated the default version of the Java Play Billing library that the Kotlin extension depends on. - -## 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). - -### 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. - -To learn more, see [Use Google Play Billing with -Unity](https://developer.android.com/google/play/billing/unity). - -### Summary of changes - -- Java Google Play Billing Library - - 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 [`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). - -## 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). - -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. - -## Google Play Billing Library 2.0.3 Release (2019-08-05) - -Version 2.0.3 of the Google Play Billing library is now available. - -### Bug fixes - -- Fixed a bug where `querySkuDetailsAsync()` would occasionally fail with code `DEVELOPER_ERROR` instead of returning a successful result. - -## 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. - -## 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. - -### Bug fixes - -- Fixed a bug where debug messages were being returned as `null` in some cases. -- Fixed a potential memory leak issue. - -## 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. - -### Purchases must be acknowledged within three days - -> [!NOTE] -> **Note:** This section describes a new requirement for acknowledging all purchases. If you are already consuming purchases using `consumeAsync()`, you don't need to make any further changes if you consume purchases within 3 days, as `consumeAsync()` automatically acknowledges a purchase. Failure to properly acknowledge purchases will result in purchases being refunded. - -> [!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. - -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. - -You can acknowledge a purchase by using one of the following methods: - -- For consumable products, use `consumeAsync()`, found in the client API. -- For products that aren't consumed, use `acknowledgePurchase()`, found in the client API. -- A new `acknowledge()` method is also available in the Server API. - -### 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). - -> [!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). - -### 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. - -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. - -### Consistent offers - -> [!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. - -[`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. -- [`getOriginalPrice()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getOriginalPrice()) - - returns the original price with additional currency formatting. - -### 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. - -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: - -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. - -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`. - -> [!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. - -#### 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. - -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. - -#### 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. - -### Bug fixes - -- [`SkuDetails.getIntroductoryPriceAmountMicros()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getIntroductoryPriceAmountMicros()) now returns a `long` instead of a `String`. - -## 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. - -### Bug fixes - -- Fixed a threading issue introduced in v1.2.1. Background calls no longer block the main thread. - -### Other changes - -- Although using the main thread is still recommended, you can now instantiate the Google Play Billing Library from a background thread. -- Instantiation has been fully migrated to the background thread to reduce the chance of causing ANRs. - -## 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. - -### 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). - -### Other changes - -- Added public constructors for `PurchasesResult` and `SkuDetailsResult` to make testing easier. -- `SkuDetails` objects can use a new method, `getOriginalJson()`. -- All AIDL service calls are now handled by background threads. - -### Bug fixes - -- Null callback listeners are no longer passed into public APIs. - -## 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. - -### 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). -- 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()`. -- Minor bug fixes and code optimizations. - -#### 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. - -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 - -```kotlin -val priceChangeFlowParams = PriceChangeFlowParams.newBuilder() - .setSkuDetails(skuDetailsOfThePriceChangedSubscription) - .build() - -billingClient.launchPriceChangeConfirmationFlow(activity, - priceChangeFlowParams, - object : PriceChangeConfirmationListener() { - override fun onPriceChangeConfirmationResult(responseCode: Int) { - // Handle the result. - } - }) -``` - -### Java - -```java -PriceChangeFlowParams priceChangeFlowParams = - PriceChangeFlowParams.newBuilder() - .setSkuDetails(skuDetailsOfThePriceChangedSubscription) - .build(); - -billingClient.launchPriceChangeConfirmationFlow(activity, - priceChangeFlowParams, - new PriceChangeConfirmationListener() { - @Override - public void onPriceChangeConfirmationResult(int responseCode) { - // Handle the result. - } - }); -``` - -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). - -#### 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. - -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. - -### Kotlin - -```kotlin -private lateinit var mBillingClient: BillingClient -private val mSkuDetailsMap = HashMap() - -private fun querySkuDetails() { - val skuDetailsParamsBuilder = SkuDetailsParams.newBuilder() - mBillingClient.querySkuDetailsAsync(skuDetailsParamsBuilder.build() - ) { responseCode, skuDetailsList -> - if (responseCode == 0) { - for (skuDetails in skuDetailsList) { - mSkuDetailsMap[skuDetails.sku] = skuDetails - } - } - } -} - -private fun startPurchase(skuId: String) { - val billingFlowParams = BillingFlowParams.newBuilder() - .setSkuDetails(mSkuDetailsMap[skuId]) - .build() -} -``` - -### Java - -```java -private BillingClient mBillingClient; -private Map mSkuDetailsMap = new HashMap<>(); - -private void querySkuDetails() { - SkuDetailsParams.Builder skuDetailsParamsBuilder - = SkuDetailsParams.newBuilder(); - mBillingClient.querySkuDetailsAsync(skuDetailsParamsBuilder.build(), - new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(int responseCode, - List skuDetailsList) { - if (responseCode == 0) { - for (SkuDetails skuDetails : skuDetailsList) { - mSkuDetailsMap.put(skuDetails.getSku(), skuDetails); - } - } - } - }); -} - -private void startPurchase(String skuId) { - BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() - .setSkuDetails(mSkuDetailsMap.get(skuId)) - .build(); -} -``` - -## 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. - -### Summary of changes - -- Added support to specify a proration mode in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) when upgrading/downgrading an existing subscription. -- The `replaceSkusProration` boolean flag in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) is no longer supported. Use `replaceSkusProrationMode` instead. -- [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) now triggers a callback for failed responses. - -### 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. - -### Kotlin - -```kotlin -BillingFlowParams.newBuilder() - .setSku(skuId) - .setType(billingType) - .setOldSku(oldSku) - .setReplaceSkusProrationMode(replaceSkusProrationMode) - .build() -``` - -
- -
- -### Java - -```java -BillingFlowParams.newBuilder() - .setSku(skuId) - .setType(billingType) - .setOldSku(oldSku) - .setReplaceSkusProrationMode(replaceSkusProrationMode) - .build(); -``` - -
- -Google Play supports following proration modes: - -|---|---| -| `IMMEDIATE_WITH_TIME_PRORATION` | Replacement takes effect immediately, and the new expiration time will be prorated and credited or charged to the user. This is the current default behavior. | -| `IMMEDIATE_AND_CHARGE_PRORATED_PRICE` | Replacement takes effect immediately, and the billing cycle remains the same. The price for the remaining period will be charged. **Note**: This option is only available for subscription upgrade. | -| `IMMEDIATE_WITHOUT_PRORATION` | Replacement takes effect immediately, and the new price will be charged on next recurrence time. The billing cycle stays the same. | - -### `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. - -### [`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. - -### Bug fixes - -- Properly exits early in async methods when service is disconnected. -- [`Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder) param objects no longer mutates built objects. -- Issue [68087141](https://issuetracker.google.com/issues/68087141): [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) now trigger callback for failed responses. - -## 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. - -### Important changes - -- Embedded billing permission inside library's manifest. It's not necessary to add the `com.android.vending.BILLING` permission inside Android manifest anymore. -- New builder added to [`BillingClient.Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html) class. -- Introduced builder pattern for [`SkuDetailsParams`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetailsParams.html) class to be used on methods to query SKUs. -- Several API methods were updated for consistency (the same return argument names and order). - -### 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: - -### Kotlin - -```kotlin -billingClient = BillingClient.newBuilder(context).setListener(this).build() -``` - -### Java - -```java -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: - -### Kotlin - -```kotlin -BillingFlowParams.newBuilder().setSku(skuId) - .setType(billingType) - .setOldSku(oldSku) - .build() - -// Then, use the BillingFlowParams to start the purchase flow -val responseCode = billingClient.launchBillingFlow(builder.build()) -``` - -### Java - -```java -BillingFlowParams.newBuilder().setSku(skuId) - .setType(billingType) - .setOldSku(oldSku) - .build(); - -// Then, use the BillingFlowParams to start the purchase flow -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: - -### Kotlin - -```kotlin -val params = SkuDetailsParams.newBuilder() -params.setSkusList(skuList) - .setType(itemType) -billingClient.querySkuDetailsAsync(params.build(), object : SkuDetailsResponseListener() { - ... -}) -``` - -### Java - -```java -SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); -params.setSkusList(skuList) - .setType(itemType); -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: - -### Kotlin - -```kotlin -fun onSkuDetailsResponse(@BillingResponse responseCode: Int, skuDetailsList: List) -``` - -### Java - -```java -public void onSkuDetailsResponse(@BillingResponse int responseCode, List skuDetailsList) -``` - -#### Parameters order changed on `onConsumeResponse()` method - -The order of arguments for -[`onConsumeResponse`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeResponseListener.html#onConsumeResponse(int,%0Ajava.lang.String)) from the -[`ConsumeResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeResponseListener.html) -interface has changed to be consistent across our API: - -### Kotlin - -```kotlin -fun onConsumeResponse(@BillingResponse responseCode: Int, outToken: String) -``` - -### Java - -```java -public void onConsumeResponse(@BillingResponse int responseCode, String outToken) -``` - -#### Unwrapped PurchaseResult object - -[`PurchaseResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult.html) -has been unwraped to be consistent across our API: - -### Kotlin - -```kotlin -fun onPurchaseHistoryResponse(@BillingResponse responseCode: Int, purchasesList: List) -``` - -### Java - -```java -void onPurchaseHistoryResponse(@BillingResponse int responseCode, List purchasesList) -``` - -### 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) - -## 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. - -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 deleted file mode 100644 index 227a537..0000000 --- a/play/play-billing-library-version-upgrade/references/migration-logic.md +++ /dev/null @@ -1,78 +0,0 @@ -## High-Impact Migration Logic - -### 1. Connection Management (The v8+ Reconnection Shift) - -**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`. -- **Logic** : In v8+, the library handles transient disconnections. Your `onServiceDisconnected` must only be used for logging or updating UI state (e.g., "Billing service temporarily unavailable"). - -### 2. Product Querying \& Models (The v5-v8 Architectural Shift) - -**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). - - **Modern** : `ProductDetails`. A single `ProductDetails` can contain multiple `SubscriptionOfferDetails` (Base Plans + Offers). -- **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... - }` - -### 3. Subscription Modernization (v6 \& v7) - -**Intent**: Support "Base Plans" and "Offers" instead of legacy standalone SKUs. - -- **Subscription Upgrades/Downgrades (v6+)**: - - - **Legacy** : `setOldSkuPurchaseToken()` in `BillingFlowParams`. - - **Modern** : Use `SubscriptionUpdateParams`. You must specify the `PurchaseToken` of the existing subscription and the `ReplacementMode` (which replaces the deprecated `ProrationMode`). - - **Logic** : Verify that the `ReplacementMode` matches the business intent (e.g., `CHARGE_FULL_PRICE` versus `WITH_TIME_PRORATION`). -- **Installment Plans (v7+)**: - - - **Intent**: Allow users to pay for a subscription in monthly installments. - - **Check** : Look for `InstallmentPlanDetails` within `SubscriptionOfferDetails`. If the app supports high-ticket subscriptions, it is mandatory to implement the `installmentPlanDetails` UI. - -### 4. Purchase Handling \& History (v6+) - -**Intent**: Move away from local-only purchase caches to real-time status -checks. - -- **Active Purchases** : - - **Deprecated** : `queryPurchases()` (synchronous). - - **Mandatory** : `queryPurchasesAsync()`. You must pass `QueryPurchasesParams` containing the `ProductType` (`INAPP` or `SUBS`). -- **Purchase History (Pagination Intent)** : - - **v6+ Change** : `queryPurchaseHistoryAsync` is optimized for pagination. - - **Logic** : If the app has thousands of historical transactions, verify you are using the `PurchaseHistoryRecord` list correctly to avoid memory overhead. - -### 5. Security \& Pending Transactions (The "Always On" Rule) - -- **Mandatory** : `enablePendingPurchases()` has been required since v3, but in v8+, verify that it is called before `.build()`. You must also include `.enableOneTimeProducts()` on the `enablePendingPurchases()` builder. -- **Optional** : If the app sells prepaid subscriptions, you must also include `.enablePrepaidPlans()`. -- **Intent**: This handles "Slow/Delayed" payments (like cash or bank transfers). Without this, the app will crash on initialization in modern versions. - -### 6. SDK \& Environment Requirements - -- **PBL 7.0** : Requires `compileSdk 34` or higher. -- **PBL 8.0** : Requires `compileSdk 34` or higher. -- **PBL 9.0** : Requires `compileSdk 35` or higher. -- **Kotlin** : Verify that `kotlin-stdlib` is updated to at least 1.9.x to support new library coroutine extensions. - -### 7. User-Facing Features (Post-Upgrade Recommendations) - -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). -- **v8** : **Personalized Pricing** (Show legal disclosure if price varies by user). -- **v9** : **In-App Price Reviews** (Users can accept upcoming subscription price increases directly inside the app without leaving for the Play Store; capped by Google at a maximum of once every 7 days). \ No newline at end of file diff --git a/play/play-billing-library-version-upgrade/references/version-checklist.md b/play/play-billing-library-version-upgrade/references/version-checklist.md deleted file mode 100644 index 98f5831..0000000 --- a/play/play-billing-library-version-upgrade/references/version-checklist.md +++ /dev/null @@ -1,54 +0,0 @@ -## 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. - -## PBL v1.x through v3.x - -- \[ \] **\[v1.0\] Builder Pattern** : Verify `BillingClient.newBuilder(context)` is used. -- \[ \] **\[v2.0\] Mandatory Acknowledgment** : Verify `acknowledgePurchase()` or `consumeAsync()` is called within 3 days. -- \[ \] **\[v2.0\] Response Types** : Logic must handle `BillingResult` objects instead of raw integers. -- \[ \] **\[v3.0\] Legacy Removal** : Verify `ChildDirected` and `UnderAgeOfConsent` parameters are deleted. - -## PBL v4.x Series - -- \[ \] **Async Purchasing** : Confirm `queryPurchases()` is replaced with `queryPurchasesAsync()`. -- \[ \] **Multi-SKU Accessors** : Replace `getSku()` with `getSkus()` (returns a list) in `Purchase` objects. -- \[ \] **Subscription Refactor** : Verify `setSubscriptionUpdateParams()` is used for change logic. - -## PBL v5.x Series - -- \[ \] **Data Model Swap** : Replace all instances of `SkuDetails` with `ProductDetails`. -- \[ \] **Personalized Pricing** : Implement `setIsOfferPersonalized()` for EU price disclosures. - -## PBL v6.x Series - -- \[ \] **Replacement Mode** : Replace `ProrationMode` with the `ReplacementMode` enum. -- \[ \] **User Choice Billing** : Replace `AlternativeBillingListener` with `UserChoiceBillingListener`. - -## PBL v7.x Series - -- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher. -- \[ \] **Pending Purchases** : Replace parameterless `enablePendingPurchases()` with `enablePendingPurchases(PendingPurchaseParams)`. -- \[ \] **API Cleanup** : Replace `setOldSkuPurchaseToken()` with `setOldPurchaseToken()`. - -## PBL v8.x Series - -- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher. -- \[ \] **Terminology Shift**: Rename "in-app items" to "one-time products" in UI/strings. -- \[ \] **Signature Enforcement** : `onProductDetailsResponse` signature MUST be `(BillingResult, QueryProductDetailsResult)`. -- \[ \] **SUBS Offer Token** : For `SUBS` type, `ProductDetailsParams` MUST include an `offerToken`. -- \[ \] **Auto-Reconnection** : Verify `enableAutoServiceReconnection()` is used in the builder. -- \[ \] **Min SDK Increase** : Verify `minSdkVersion` is at least 23. - -## PBL v9.x Series - -- \[ \] **SDK Compliance** : `compileSdk` is set to 35 or higher. -- \[ \] **API Removals** : Ensure all usages of `SkuDetails`, `SkuDetailsParams`, and `SkuDetailsResponseListener` are removed and replaced with their `ProductDetails` equivalents. -- \[ \] **Blocked Store Error Handling** : If targeting restricted device modes (e.g., kids modes), ensure `androidx.core` is at v1.9+ and update error logic to catch `BILLING_UNAVAILABLE` instead of `ERROR`. -- \[ \] **External Payment Program Null-Safety** : If using developer-provided billing, update `DeveloperProvidedBillingDetails.getLinkUri()` logic to explicitly check for both `null` and empty string (`""`) values before parsing. - -## Future Versions (PBL 10.0.0+) - -- \[ \] **Dynamic Checklist Generation** : For any version \>=10.0.0, you **MUST** synthesize a new checklist for each new version header found in the [Release Notes](https://developer.android.com/google/play/billing/release-notes). -- \[ \] **Identify Version Delta** : Review "Breaking Changes" and "Removed APIs" for the new version and create a list of terms to `grep`. \ No newline at end of file diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md index 8d21d93..071ba14 100644 --- a/wear/wear-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -9,7 +9,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-18' + last-updated: '2026-07-31' keywords: - Wear OS - Compose @@ -67,66 +67,117 @@ features. ### Capability 3: Component samples -Wear Compose includes individual component samples for each and every component, -within the `--samples-sources.jar` file. Gradle automatically -downloads these JAR files along with the main library JAR when using any of -`compose-material3`, `compose-foundation` or `compose-navigation3`. +Use this table of reference to find canonical samples for Wear Compose +components. +When working with a Wear Compose component, you must use the samples linked +from the table to ensure you know how to correctly use it. -Use the canonical component samples whenever adding or adjusting a Wear Compose -Material3 component. +#### Material 3 components in `androidx.wear.compose.material3.*` -STRICT COMPLIANCE: Extraction is NOT optional. You are FORBIDDEN from -implementing any code until samples are extracted and read. Bypassing this step -with alternative search tools or by assuming library documentation is sufficient -is a protocol breach. You MUST verify the local cache by reading a sample file -before proceeding. +| Component / Symbol | Reference Samples | +|---|---| +| `AlertDialog`, `AlertDialogDefaults` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt) | +| `AnimatedPage`, `HorizontalPagerScaffold`, `VerticalPagerScaffold` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | +| `AnimatedText`, `rememberAnimatedTextFontRegistry` | [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt) | +| `AppCard` | [CardSample](references/material3/CardSample.kt.md.txt) | +| `AppScaffold` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ArcProgressIndicator`, `ArcProgressIndicatorDefaults`, `CircularProgressIndicator`, `CircularProgressIndicatorDefaults`, `ProgressIndicatorDefaults`, `SegmentedCircularProgressIndicator`, `drawCircularProgressIndicator` | [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | +| `Button` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt), [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [PickerSample](references/material3/PickerSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ButtonGroup` | [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | +| `Card` | [CardSample](references/material3/CardSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `CardDefaults` | [CardSample](references/material3/CardSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `CheckboxButton` | [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | +| `ChildButton`, `OutlinedButton` | [ButtonSample](references/material3/ButtonSample.kt.md.txt) | +| `ColorScheme`, `dynamicColorScheme` | [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt) | +| `CompactButton`, `CompactButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ConfirmationDialog`, `ConfirmationDialogDefaults`, `FailureConfirmationDialog`, `FavoriteIcon`, `SuccessConfirmationDialog`, `confirmationDialogCurvedText` | [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt) | +| `CurvedTextDefaults` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt) | +| `DatePicker`, `DatePickerType` | [DatePickerSample](references/material3/DatePickerSample.kt.md.txt) | +| `EdgeButton` | [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `EdgeButtonSize` | [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt) | +| `FadingExpandingLabel` | [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt) | +| `FilledIconButton`, `FilledTonalIconButton`, `IconButtonColors`, `IconButtonShapes`, `OutlinedIconButton` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt) | +| `FilledTonalButton` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | +| `GestureAction`, `OneHandedGestureClickIndicator`, `OneHandedGestureClickIndicatorState`, `oneHandedGesture`, `rememberOneHandedGestureConfiguration` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) | +| `GesturePriority`, `LocalOneHandedGestureEnabled`, `OneHandedGestureDefaults`, `OneHandedGestureHorizontalPageIndicator`, `OneHandedGesturePageIndicatorState`, `OneHandedGestureScrollIndicator`, `OneHandedGestureScrollIndicatorState`, `OneHandedGestureVerticalPageIndicator` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) | +| `HeadphoneIcon`, `Stepper`, `StepperLevelIndicator`, `rangeSemantics` | [StepperSample](references/material3/StepperSample.kt.md.txt) | +| `HorizontalPageIndicator`, `VerticalPageIndicator` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt) | +| `Icon` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt) | +| `IconButton` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | +| `IconButtonDefaults` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | +| `IconToggleButton`, `IconToggleButtonDefaults`, `WifiOffIcon`, `WifiOnIcon` | [IconToggleButtonSample](references/material3/IconToggleButtonSample.kt.md.txt) | +| `LevelIndicator` | [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt) | +| `LinearProgressIndicator` | [LinearProgressIndicatorSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt) | +| `ListHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ListHeaderDefaults` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ListSubHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt) | +| `MaterialTheme` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [LinearProgressIndicatorSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt) | +| `OpenOnPhoneDialog`, `OpenOnPhoneDialogDefaults`, `openOnPhoneDialogCurvedText` | [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt) | +| `OutlinedCard` | [CardSample](references/material3/CardSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `PagerScaffoldDefaults` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | +| `Picker` | [PickerSample](references/material3/PickerSample.kt.md.txt) | +| `PickerGroup` | [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt) | +| `RadioButton`, `SplitRadioButton` | [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt) | +| `ResponsiveTransformationSpec`, `TransformationVariableSpec` | [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | +| `RevealValue`, `SwipeToReveal`, `SwipeToRevealDefaults`, `rememberRevealState` | [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | +| `ScreenScaffold` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `ScreenScaffoldDefaults`, `ScrollIndicator` | [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt) | +| `ScreenStage`, `scrollAway` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt) | +| `Slider`, `SliderDefaults` | [SliderSample](references/material3/SliderSample.kt.md.txt) | +| `SplitCheckboxButton` | [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt) | +| `SplitSwitchButton` | [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt) | +| `StepperDefaults`, `VolumeDownIcon`, `VolumeUpIcon` | [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt) | +| `SurfaceTransformation` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `SwipeToDismissBox` | [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | +| `SwitchButton` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt) | +| `Text` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt), [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt), [PickerSample](references/material3/PickerSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `TextButton` | [TextButtonSample](references/material3/TextButtonSample.kt.md.txt) | +| `TextButtonDefaults` | [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt) | +| `TextToggleButton`, `TextToggleButtonDefaults`, `touchTargetAwareSize` | [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt) | +| `TimePicker`, `TimePickerType` | [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt) | +| `TimeText` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +| `TimeTextDefaults`, `timeTextCurvedText` | [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +| `TitleCard` | [CardSample](references/material3/CardSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | +| `TransformationSpec` | [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | +| `curvedText` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +| `firstVisibleItemLayoutItemInfo`, `layoutItemInfoOf`, `rememberTransformingLazyColumnFirstLayoutItemProvider` | [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `placeholder`, `placeholderShimmer`, `rememberPlaceholderState` | [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt) | +| `rememberPickerState` | [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt), [PickerSample](references/material3/PickerSample.kt.md.txt) | +| `rememberTransformationSpec`, `transformedHeight` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `timeTextSeparator` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | -Exception: You don't need to extract samples if the request is strictly related -to tooling (for example, adding @WearPreviewDevices), updating text/colors, or -basic refactoring that doesn't involve adding new Wear Compose components. +#### Foundation components in `androidx.wear.compose.foundation.*` -#### Step 1: Prepare - -1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. -2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. - -#### Step 2: Check the local cache - -1. Define the cache directory path: `/tmp/wear-compose-samples/{VERSION}/`. Do NOT choose your own different location. -2. Check if this directory exists and contains subdirectories with `.kt` files. - - **IF YES (cache hit):** Proceed to **Step 4**. - - **IF NO (cache miss):** Proceed to **Step 3**. - -#### Step 3: Network-based sample retrieval - -Google Maven publishes a `-samples-sources.jar` alongside every library release. -Download and extract it directly without needing a local Gradle sync or cache -lookup. - -1. Determine the `{VERSION}` of `androidx.wear.compose:compose-material3` from `build.gradle.kts` or `libs.versions.toml`. -2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include `navigation3` if `androidx.wear.compose.navigation3` is used. -3. For each `{ARTIFACT}`, run the following commands to download and extract - the samples: - - # Download the samples-sources JAR - curl -sSL "https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-{ARTIFACT}/{VERSION}/compose-{ARTIFACT}-{VERSION}-samples-sources.jar" -o {ARTIFACT}-{VERSION}-samples.jar - - # Extract and flatten into the cache directory - unzip -q -o {ARTIFACT}-{VERSION}-samples.jar -d /tmp/wear-compose-samples/{VERSION}/{ARTIFACT}/ - - # Clean up - rm {ARTIFACT}-{VERSION}-samples.jar - -4. If this works, proceed directly to step 4. - -#### Step 4: Read samples and implement - -1. Read the relevant `.kt` sample files. -2. Use these official, version-matched samples as the source of truth for: - - Required parameters and slot names. - - Default styling and typography tokens. - - Interactive behaviors (for example: `onClick`, `onLongClick`). - - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). +| Component / Symbol | Reference Samples | +|---|---| +| `AmbientMode`, `AmbientTickEffect`, `LocalAmbientModeManager`, `rememberAmbientModeManager` | [AmbientModeSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/foundation/AmbientModeSample.kt) | +| `AutoCenteringParams`, `ScalingLazyColumnDefaults`, `ScalingLazyListAnchorType` | [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt) | +| `BasicSwipeToDismissBox` | [SwipeToDismissBoxSample](references/foundation/SwipeToDismissBoxSample.kt.md.txt) | +| `CurvedAlignment`, `CurvedTextStyle`, `angularGradientBackground`, `angularSize`, `basicCurvedText`, `clearAndSetSemantics`, `curvedColumn`, `padding`, `radialGradientBackground`, `radialSize`, `semantics`, `size` | [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt) | +| `CurvedDirection`, `CurvedLayout`, `angularSizeDp`, `background`, `curvedBox`, `curvedComposable`, `curvedRow` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt) | +| `CurvedModifier` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +| `ExperimentalWearFoundationApi`, `RevealValue`, `SwipeToReveal`, `rememberRevealState` | [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt) | +| `HorizontalPager`, `VerticalPager`, `rememberPagerState` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerSamples](references/foundation/PagerSamples.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | +| `ItemEdge`, `TransformingLazyColumnDefaults` | [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | +| `PagerDefaults` | [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | +| `RotaryScrollableDefaults` | [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [RotarySamples](references/foundation/RotarySamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | +| `RotarySnapLayoutInfoProvider`, `rotaryScrollable` | [RotarySamples](references/foundation/RotarySamples.kt.md.txt) | +| `ScalingLazyColumn` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt), [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | +| `ScrollInfoProvider` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt) | +| `SwipeToDismissValue`, `edgeSwipeToDismiss`, `rememberSwipeToDismissBoxState` | [SwipeToDismissBoxSample](references/foundation/SwipeToDismissBoxSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | +| `TransformingLazyColumn` | [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `TransformingLazyColumnFirstLayoutItemProvider` | [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `TransformingLazyColumnItemScrollProgress` | [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | +| `expandableButton`, `expandableItems` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt) | +| `expandableItem`, `rememberExpandableState` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt), [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt) | +| `hierarchicalFocusGroup` | [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt) | +| `items` | [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt) | +| `itemsIndexed` | [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `rememberScalingLazyListState` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | +| `rememberTransformingLazyColumnState` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | +| `requestFocusOnHierarchyActive` | [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt), [RotarySamples](references/foundation/RotarySamples.kt.md.txt) | +| `weight` | [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | ### Capability 4: Component guidance diff --git a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md index 9e1498d..9ff43dc 100644 --- a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5: ### M3 - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha06") + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha07") See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). -Wear Compose Foundation library version 1.7.0-alpha06 introduced +Wear Compose Foundation library version 1.7.0-alpha07 introduced some new components that are designed to work with Material 3 components. Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an updated animation when running on Wear OS 6 (API level 36) or higher. When updating to Wear Compose Material 3 version, we suggest to also update the Wear Compose Foundation and Navigation libraries: - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha06") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha06") + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha07") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha07") ## Theme @@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components: And finally a list of some relevant components from Wear Compose Foundation library: -| Wear Compose Foundation 1.7.0-alpha06 | | +| Wear Compose Foundation 1.7.0-alpha07 | | |---|---| | [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | | [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | diff --git a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt new file mode 100644 index 0000000..e19960b --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt @@ -0,0 +1,430 @@ +``` +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.foundation.CurvedAlignment +import androidx.wear.compose.foundation.CurvedDirection +import androidx.wear.compose.foundation.CurvedLayout +import androidx.wear.compose.foundation.CurvedModifier +import androidx.wear.compose.foundation.CurvedTextStyle +import androidx.wear.compose.foundation.angularGradientBackground +import androidx.wear.compose.foundation.angularSize +import androidx.wear.compose.foundation.angularSizeDp +import androidx.wear.compose.foundation.background +import androidx.wear.compose.foundation.basicCurvedText +import androidx.wear.compose.foundation.clearAndSetSemantics +import androidx.wear.compose.foundation.curvedBox +import androidx.wear.compose.foundation.curvedColumn +import androidx.wear.compose.foundation.curvedComposable +import androidx.wear.compose.foundation.curvedRow +import androidx.wear.compose.foundation.padding +import androidx.wear.compose.foundation.radialGradientBackground +import androidx.wear.compose.foundation.radialSize +import androidx.wear.compose.foundation.semantics +import androidx.wear.compose.foundation.size +import androidx.wear.compose.foundation.weight +import androidx.wear.compose.material.Text + +@Sampled +@Composable +fun SimpleCurvedWorld() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + curvedComposable { + BasicText( + "Simple", + Modifier.background(Color.White).padding(2.dp), + TextStyle(color = Color.Black, fontSize = 16.sp), + ) + } + curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) } + curvedComposable { + BasicText( + "CurvedWorld", + Modifier.background(Color.White).padding(2.dp), + TextStyle(color = Color.Black, fontSize = 16.sp), + ) + } + } +} + +@Sampled +@Composable +fun CurvedRowAndColumn() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) } + curvedColumn(angularAlignment = CurvedAlignment.Angular.End) { + repeat(3) { + curvedRow { + curvedComposable { + BasicText( + "Row #$it", + Modifier.background(Color.White).padding(2.dp), + TextStyle(color = Color.Black, fontSize = 14.sp), + ) + } + curvedComposable { + Box(modifier = Modifier.size(10.dp).background(Color.Green)) + } + curvedComposable { + BasicText( + "More", + Modifier.background(Color.Yellow).padding(2.dp), + TextStyle(color = Color.Black, fontSize = 14.sp), + ) + } + } + } + } + curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) } + } +} + +@Sampled +@Composable +fun CurvedAndNormalText() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText( + "Curved Text", + CurvedModifier.padding(10.dp), + style = { + CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) + }, + ) + curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) } + curvedComposable { + BasicText( + "Normal Text", + Modifier.padding(5.dp), + TextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White), + ) + } + } +} + +@Sampled +@Composable +fun CurvedFixedSize() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText( + "45 deg", + style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, + modifier = + CurvedModifier.background(Color.White).size(sweepDegrees = 45f, thickness = 40.dp), + ) + basicCurvedText( + "40 dp", + style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, + modifier = + CurvedModifier.background(Color.Yellow).radialSize(40.dp).angularSizeDp(40.dp), + ) + } +} + +@Sampled +@Composable +fun CurvedBackground() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText( + "Radial", + style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, + modifier = + CurvedModifier.radialGradientBackground(0f to Color.White, 1f to Color.Black) + .padding(5.dp), + ) + basicCurvedText( + "Angular", + style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, + modifier = + CurvedModifier.angularGradientBackground(0f to Color.White, 1f to Color.Black) + .padding(5.dp), + ) + } +} + +@Sampled +@Composable +fun CurvedWeight() { + CurvedLayout(modifier = Modifier.fillMaxSize().background(Color.White)) { + // Evenly spread A, B & C in a 90 degree angle. + curvedRow(modifier = CurvedModifier.angularSize(90f)) { + basicCurvedText("A") + curvedRow(modifier = CurvedModifier.weight(1f)) {} + basicCurvedText("B") + curvedRow(modifier = CurvedModifier.weight(1f)) {} + basicCurvedText("C") + } + } +} + +@Sampled +@Composable +fun CurvedBottomLayout() { + CurvedLayout( + modifier = Modifier.fillMaxSize(), + anchor = 90f, + angularDirection = CurvedDirection.Angular.Reversed, + ) { + basicCurvedText( + "Bottom", + style = { + CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) + }, + ) + curvedComposable { Spacer(modifier = Modifier.size(5.dp)) } + basicCurvedText( + "text", + style = { + CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) + }, + ) + } +} + +@Sampled +@Composable +fun CurvedBoxSample() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + curvedBox( + modifier = CurvedModifier.background(Color.Red), + radialAlignment = CurvedAlignment.Radial.Inner, + angularAlignment = CurvedAlignment.Angular.End, + ) { + curvedComposable { + Box(modifier = Modifier.width(40.dp).height(80.dp).background(Color.Green)) + } + curvedComposable { + Box(modifier = Modifier.size(30.dp).clip(CircleShape).background(Color.White)) + } + } + } +} + +@Sampled +@Composable +fun CurvedLetterSpacingSample() { + val style = + CurvedTextStyle( + letterSpacing = 0.6.sp, + letterSpacingCounterClockwise = 1.4.sp, + color = Color.White, + ) + Box { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText("Clockwise", style = style) + } + CurvedLayout( + modifier = Modifier.fillMaxSize(), + angularDirection = CurvedDirection.Angular.CounterClockwise, + anchor = 90f, + ) { + basicCurvedText("Counter Clockwise", style = style) + } + } +} + +@Sampled +@Composable +fun CurvedWarpingSample() { + val style = + CurvedTextStyle( + letterSpacing = 0.6.sp, + letterSpacingCounterClockwise = 1.4.sp, + color = Color.White, + ) + Box { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText( + "No Warping", + style = style.copy(warpOffset = CurvedTextStyle.WarpOffset.None), + ) + } + CurvedLayout( + modifier = Modifier.fillMaxSize(), + angularDirection = CurvedDirection.Angular.CounterClockwise, + anchor = 90f, + ) { + basicCurvedText( + "Standard Warping", + style.copy(warpOffset = CurvedTextStyle.WarpOffset.HalfOpticalHeight), + ) + } + } +} + +@Sampled +@Composable +fun CurvedSemanticsSample() { + val style = + CurvedTextStyle( + letterSpacing = 0.6.sp, + letterSpacingCounterClockwise = 1.4.sp, + color = Color.White, + ) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText( + "2 - Left", + style, + CurvedModifier.semantics { + contentDescription = "Left" + traversalIndex = 2f + }, + ) + curvedComposable { Box(Modifier.padding(5.dp).background(Color.Red).size(5.dp)) } + basicCurvedText( + "3 - Right", + style, + CurvedModifier.semantics { + contentDescription = "Right" + traversalIndex = 3f + }, + ) + } + Row { + Text("Text 1", Modifier.semantics { traversalIndex = 1f }) + Spacer(Modifier.size(10.dp)) + Text("Text 4", Modifier.semantics { traversalIndex = 4f }) + } + } +} + +@Sampled +@Composable +fun CurvedClearSemanticsSample() { + val style = + CurvedTextStyle( + letterSpacing = 0.6.sp, + letterSpacingCounterClockwise = 1.4.sp, + color = Color.White, + ) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + basicCurvedText("This is not announced", style, CurvedModifier.clearAndSetSemantics {}) + } + Row { Text("This is announced", Modifier.semantics { traversalIndex = -1f }) } + } +} + +@Composable +fun CurvedFontWeight() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + (100..900 step 100).forEach { + basicCurvedText( + "W$it", + style = CurvedTextStyle(color = Color.White, fontWeight = FontWeight(it)), + modifier = CurvedModifier.padding(5.dp), + ) + } + } +} + +@Composable +fun CurvedFontHeight() { + Box( + modifier = + Modifier.aspectRatio(1f) + .fillMaxSize() + .padding(2.dp) + .border(2.dp, Color.White, CircleShape) + ) { + CurvedLayout() { + basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp)) + } + CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.CounterClockwise) { + basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp)) + } + } +} + +@Composable +fun CurvedFonts() { + CurvedLayout(modifier = Modifier.fillMaxSize()) { + listOf( + "Serif" to FontFamily.Serif, + "SansSerif" to FontFamily.SansSerif, + "Monospace" to FontFamily.Monospace, + "Cursive" to FontFamily.Cursive, + ) + .forEach { (name, ff) -> + basicCurvedText( + "$name", + style = CurvedTextStyle(color = Color.White, fontFamily = ff), + modifier = CurvedModifier.padding(5.dp), + ) + } + } +} + +@Composable +fun OversizeComposable() { + val modBase = CurvedModifier.size(sweepDegrees = 30f, thickness = 20.dp) + CurvedLayout(modifier = Modifier.fillMaxSize()) { + curvedComposable(modifier = modBase.background(Color.Red)) {} + curvedComposable(modifier = modBase.background(Color.Green)) { + Box(Modifier.size(80.dp, 30.dp).background(Color.White)) + } + curvedComposable(modifier = modBase.background(Color.Blue)) {} + } + CurvedLayout(modifier = Modifier.fillMaxSize(), anchor = 90f) { + curvedComposable(modifier = CurvedModifier.background(Color.Green)) { + Box(Modifier.size(80.dp, 30.dp).background(Color.White)) + } + } +} + +@Composable +fun CurvedLineHeight() { + val baseStyle = CurvedTextStyle(color = Color.White, background = Color.Gray, fontSize = 16.sp) + CurvedLayout( + modifier = Modifier.fillMaxSize(), + radialAlignment = CurvedAlignment.Radial.Center, + ) { + basicCurvedText("Line Height 10.sp", style = baseStyle.copy(lineHeight = 10.sp)) + curvedBox(CurvedModifier.angularSizeDp(1.dp)) {} + basicCurvedText("Base", style = baseStyle) + curvedBox(CurvedModifier.angularSizeDp(1.dp)) {} + basicCurvedText("Line Height 24.sp", style = baseStyle.copy(lineHeight = 24.sp)) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt new file mode 100644 index 0000000..84fe733 --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt @@ -0,0 +1,99 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.expandableButton +import androidx.wear.compose.foundation.expandableItem +import androidx.wear.compose.foundation.expandableItems +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.rememberExpandableState +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.OutlinedCompactChip +import androidx.wear.compose.material.Text + +@Sampled +@Composable +fun ExpandableWithItemsSample() { + val expandableState = rememberExpandableState() + + val sampleItem: @Composable (String) -> Unit = { label -> + Chip(label = { Text(label) }, onClick = {}, secondaryLabel = { Text("line 2 - Secondary") }) + } + + val items = List(10) { "Item $it" } + val top = items.take(3) + val rest = items.drop(3) + + ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { + items(top.size) { sampleItem(top[it]) } + expandableItems(expandableState, rest.size) { sampleItem(rest[it]) } + expandableButton(expandableState) { + OutlinedCompactChip( + label = { + Text("Show More") + Spacer(Modifier.size(6.dp)) + Icon(painterResource(R.drawable.ic_expand_more_24), "Expand") + }, + onClick = { expandableState.expanded = true }, + ) + } + } +} + +@Sampled +@Composable +fun ExpandableTextSample() { + val expandableState = rememberExpandableState() + + ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { + expandableItem(expandableState) { expanded -> + Text( + "Account Alert: you have made a large purchase.\n" + + "We have noticed that a large purchase was charged to " + + "your credit card account. " + + "Please contact us if you did not perform this purchase. " + + "Our Customer Service team is available 24 hours a day, " + + "7 days a week to answer your account or product support question.", + maxLines = if (expanded) 20 else 3, + modifier = Modifier.padding(horizontal = 10.dp), + ) + } + + expandableButton(expandableState) { + OutlinedCompactChip( + label = { + Text("Show More") + Spacer(Modifier.size(6.dp)) + Icon(painterResource(R.drawable.ic_expand_more_24), "Expand") + }, + onClick = { expandableState.expanded = true }, + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt new file mode 100644 index 0000000..1df8a80 --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt @@ -0,0 +1,141 @@ +``` +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.foundation.hierarchicalFocusGroup +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.requestFocusOnHierarchyActive + +@Sampled +@Composable +fun HierarchicalFocusSample() { + var selected by remember { mutableIntStateOf(0) } + + Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { + repeat(5) { colIx -> + Box( + Modifier.hierarchicalFocusGroup(active = selected == colIx) + .weight(1f) + .clickable { selected = colIx } + .then( + if (selected == colIx) { + Modifier.border(BorderStroke(2.dp, Color.Red)) + } else { + Modifier + } + ) + ) { + // This is used a Gray background to the currently focused item, as seen by the + // focus system. + var focused by remember { mutableStateOf(false) } + + BasicText( + "$colIx", + style = + TextStyle( + color = Color.White, + fontSize = 20.sp, + textAlign = TextAlign.Center, + ), + modifier = + Modifier.fillMaxWidth() + .requestFocusOnHierarchyActive() + .onFocusChanged { focused = it.isFocused } + .focusable() + .then( + if (focused) { + Modifier.background(Color.Gray) + } else { + Modifier + } + ), + ) + } + } + } +} + +@Sampled +@Composable +fun HierarchicalFocus2Levels() { + Column(Modifier.fillMaxSize()) { + var selectedRow by remember { mutableIntStateOf(0) } + repeat(2) { rowIx -> + Row( + Modifier.weight(1f) + .fillMaxWidth() + .hierarchicalFocusGroup(active = selectedRow == rowIx) + ) { + var selectedItem by remember { mutableIntStateOf(0) } + repeat(2) { itemIx -> + Box( + Modifier.weight(1f).hierarchicalFocusGroup(active = selectedItem == itemIx) + ) { + // ScalingLazyColumn uses requestFocusOnHierarchyActive internally + ScalingLazyColumn( + Modifier.fillMaxWidth().clickable { + selectedRow = rowIx + selectedItem = itemIx + } + ) { + val prefix = (rowIx * 2 + itemIx + 'A'.code).toChar() + items(20) { + BasicText( + "$prefix $it", + style = + TextStyle( + color = Color.White, + fontSize = 20.sp, + textAlign = TextAlign.Center, + ), + ) + } + } + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt b/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt new file mode 100644 index 0000000..bae406c --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt @@ -0,0 +1,56 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.wear.compose.foundation.pager.HorizontalPager +import androidx.wear.compose.foundation.pager.VerticalPager +import androidx.wear.compose.foundation.pager.rememberPagerState + +@Sampled +@Composable +fun SimpleHorizontalPagerSample() { + // Creates a horizontal pager with 10 elements + val state = rememberPagerState { 10 } + HorizontalPager(modifier = Modifier.fillMaxSize(), state = state) { page -> + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + BasicText(text = "Page $page", style = TextStyle(color = Color.White)) + } + } +} + +@Sampled +@Composable +fun SimpleVerticalPagerSample() { + // Creates a vertical pager with 10 elements + val state = rememberPagerState { 10 } + VerticalPager(modifier = Modifier.fillMaxSize(), state = state) { page -> + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + BasicText(text = "Page $page", style = TextStyle(color = Color.White)) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt b/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt new file mode 100644 index 0000000..d15792b --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt @@ -0,0 +1,153 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.overscroll +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastSumBy +import androidx.wear.compose.foundation.requestFocusOnHierarchyActive +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults +import androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider +import androidx.wear.compose.foundation.rotary.rotaryScrollable +import androidx.wear.compose.material.Text + +@Sampled +@Composable +fun RotaryScrollSample() { + val scrollableState = rememberLazyListState() + val focusRequester = remember { FocusRequester() } + LazyColumn( + modifier = + Modifier.fillMaxSize() + .requestFocusOnHierarchyActive() + .rotaryScrollable( + behavior = RotaryScrollableDefaults.behavior(scrollableState), + focusRequester = focusRequester, + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = scrollableState, + ) { + items(300) { + BasicText( + text = "item $it", + modifier = Modifier.background(Color.Gray), + style = TextStyle.Default.copy(), + ) + } + } +} + +@Sampled +@Composable +fun RotaryScrollWithOverscrollSample() { + val scrollableState = rememberScrollState() + val focusRequester = remember { FocusRequester() } + val overscrollEffect = rememberOverscrollEffect() + + val screenHeightDp = LocalConfiguration.current.screenHeightDp.dp + + Column( + Modifier.fillMaxSize() + .requestFocusOnHierarchyActive() + .rotaryScrollable( + behavior = RotaryScrollableDefaults.behavior(scrollableState), + focusRequester = focusRequester, + overscrollEffect = overscrollEffect, + ) + .verticalScroll(scrollableState, overscrollEffect) + .overscroll(overscrollEffect), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Top") + Spacer(modifier = Modifier.height(screenHeightDp / 2)) + Text("Scroll this list up and down with rotary input", textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(screenHeightDp / 2)) + Text("Bottom") + } +} + +@Sampled +@Composable +fun RotarySnapSample() { + val scrollableState = rememberLazyListState() + val focusRequester = remember { FocusRequester() } + LazyColumn( + modifier = + Modifier.fillMaxSize() + .requestFocusOnHierarchyActive() + .rotaryScrollable( + behavior = + RotaryScrollableDefaults.snapBehavior( + scrollableState, + // This sample has a custom implementation of + // RotarySnapLayoutInfoProvider which is required for snapping behavior. + // ScalingLazyColumn has it built-in, so it's not required there. + remember(scrollableState) { + object : RotarySnapLayoutInfoProvider { + + override val averageItemSize: Float + get() { + val items = scrollableState.layoutInfo.visibleItemsInfo + return (items.fastSumBy { it.size } / items.size) + .toFloat() + } + + override val currentItemIndex: Int + get() = scrollableState.firstVisibleItemIndex + + override val currentItemOffset: Float + get() = + scrollableState.firstVisibleItemScrollOffset.toFloat() + + override val totalItemCount: Int + get() = scrollableState.layoutInfo.totalItemsCount + } + }, + ), + focusRequester = focusRequester, + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = scrollableState, + ) { + items(300) { + BasicText(text = "item $it", modifier = Modifier.background(Color.Gray).height(50.dp)) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt new file mode 100644 index 0000000..8b1c765 --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt @@ -0,0 +1,131 @@ +``` +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.AutoCenteringParams +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults +import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.ListHeader +import androidx.wear.compose.material.Text +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun SimpleScalingLazyColumn() { + ScalingLazyColumn(modifier = Modifier.fillMaxWidth()) { + item { ListHeader { Text(text = "List Header") } } + items(20) { + Chip( + onClick = {}, + label = { Text("List item $it") }, + colors = ChipDefaults.secondaryChipColors(), + ) + } + } +} + +@Sampled +@Composable +fun SimpleScalingLazyColumnWithSnap() { + val state = rememberScalingLazyListState() + ScalingLazyColumn( + rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state), + flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state), + modifier = Modifier.fillMaxWidth(), + state = state, + ) { + item { ListHeader { Text(text = "List Header") } } + items(20) { + Chip( + onClick = {}, + label = { Text("List item $it") }, + colors = ChipDefaults.secondaryChipColors(), + ) + } + } +} + +@Sampled +@Composable +fun ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo() { + val coroutineScope = rememberCoroutineScope() + val itemSpacing = 6.dp + // Line up the gap between the items on the center-line + val scrollOffset = with(LocalDensity.current) { -(itemSpacing / 2).roundToPx() } + val state = + rememberScalingLazyListState( + initialCenterItemIndex = 1, + initialCenterItemScrollOffset = scrollOffset, + ) + + ScalingLazyColumn( + modifier = Modifier.fillMaxWidth(), + anchorType = ScalingLazyListAnchorType.ItemStart, + verticalArrangement = Arrangement.spacedBy(itemSpacing), + state = state, + autoCentering = AutoCenteringParams(itemOffset = scrollOffset), + ) { + item { ListHeader { Text(text = "List Header") } } + items(20) { + Chip( + onClick = { + coroutineScope.launch { + // Add +1 to allow for the ListHeader + state.animateScrollToItem(it + 1, scrollOffset) + } + }, + label = { Text("List item $it") }, + colors = ChipDefaults.secondaryChipColors(), + ) + } + } +} + +@Sampled +@Composable +fun SimpleScalingLazyColumnWithContentPadding() { + ScalingLazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp), + autoCentering = null, + ) { + item { ListHeader { Text(text = "List Header") } } + items(20) { + Chip( + onClick = {}, + label = { Text("List item $it") }, + colors = ChipDefaults.secondaryChipColors(), + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt new file mode 100644 index 0000000..46ecc1b --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt @@ -0,0 +1,168 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.BasicSwipeToDismissBox +import androidx.wear.compose.foundation.SwipeToDismissValue +import androidx.wear.compose.foundation.edgeSwipeToDismiss +import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.SplitToggleChip +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.ToggleChipDefaults + +@Sampled +@Composable +fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) { + val state = rememberSwipeToDismissBoxState() + BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> + if (isBackground) { + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant)) + } else { + Column( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary) + } + } + } +} + +@Sampled +@Composable +fun StatefulSwipeToDismissBox() { + // State for managing a 2-level navigation hierarchy between + // MainScreen and ItemScreen composables. + // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation. + var showMainScreen by remember { mutableStateOf(true) } + val saveableStateHolder = rememberSaveableStateHolder() + + // Swipe gesture dismisses ItemScreen to return to MainScreen. + val state = rememberSwipeToDismissBoxState() + LaunchedEffect(state.currentValue) { + if (state.currentValue == SwipeToDismissValue.Dismissed) { + state.snapTo(SwipeToDismissValue.Default) + showMainScreen = !showMainScreen + } + } + + // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind + // the ItemScreen, otherwise there's no background to show. + BasicSwipeToDismissBox( + state = state, + userSwipeEnabled = !showMainScreen, + backgroundKey = if (!showMainScreen) "MainKey" else "Background", + contentKey = if (showMainScreen) "MainKey" else "ItemKey", + ) { isBackground -> + if (isBackground || showMainScreen) { + // Best practice would be to use State Hoisting and leave this composable stateless. + // Here, we want to support MainScreen being shown from different destinations + // (either in the foreground or in the background during swiping) - that can be achieved + // using SaveableStateHolder and rememberSaveable as shown below. + saveableStateHolder.SaveableStateProvider( + key = "MainKey", + content = { + // Composable that maintains its own state + // and can be shown in foreground or background. + val checked = rememberSaveable { mutableStateOf(true) } + Column( + modifier = + Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + ) { + SplitToggleChip( + checked = checked.value, + label = { Text("Item details") }, + modifier = Modifier.height(40.dp), + onCheckedChange = { v -> checked.value = v }, + onClick = { showMainScreen = false }, + toggleControl = { + Icon( + imageVector = + ToggleChipDefaults.checkboxIcon(checked = checked.value), + contentDescription = null, + ) + }, + ) + } + }, + ) + } else { + Column( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("Show details here...", color = MaterialTheme.colors.onPrimary) + Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary) + } + } + } +} + +@Sampled +@Composable +fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) { + val state = rememberSwipeToDismissBoxState() + + // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the + // modifier applies exists within a SwipeToDismissBox which shares the same state. + BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> + val horizontalScrollState = rememberScrollState(0) + if (isBackground) { + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant)) + } else { + Box(modifier = Modifier.fillMaxSize()) { + Text( + modifier = + Modifier.align(Alignment.Center) + .edgeSwipeToDismiss(state) + .horizontalScroll(horizontalScrollState), + text = + "This text can be scrolled horizontally - to dismiss, swipe " + + "right from the left edge of the screen (called Edge Swiping)", + ) + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt new file mode 100644 index 0000000..aa0e085 --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt @@ -0,0 +1,282 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWearFoundationApi::class) +@file:Suppress("DEPRECATION") + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.ExperimentalWearFoundationApi +import androidx.wear.compose.foundation.RevealValue +import androidx.wear.compose.foundation.SwipeToReveal +import androidx.wear.compose.foundation.expandableItem +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.rememberExpandableState +import androidx.wear.compose.foundation.rememberRevealState +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.ListHeader +import androidx.wear.compose.material.Text +import kotlin.math.abs +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun SwipeToRevealSample() { + val state = rememberRevealState() + val coroutineScope = rememberCoroutineScope() + SwipeToReveal( + state = state, + primaryAction = { + Box( + modifier = + Modifier.fillMaxSize().clickable { + /* Add the primary action */ + coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) } + }, + contentAlignment = Alignment.Center, + ) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete") + } + }, + undoAction = { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = { + /* Add the undo action */ + coroutineScope.launch { state.animateTo(RevealValue.Covered) } + }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Undo") }, + ) + }, + ) { + Chip( + modifier = + Modifier.fillMaxWidth().semantics { + // Use custom actions to make the primary and secondary actions accessible + customActions = + listOf( + CustomAccessibilityAction("Delete") { + /* Add the primary action click handler */ + true + } + ) + }, + onClick = { /* the click action associated with chip */ }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Swipe Me") }, + ) + } +} + +@Sampled +@Composable +fun SwipeToRevealWithDelayedText() { + val state = rememberRevealState() + val coroutineScope = rememberCoroutineScope() + SwipeToReveal( + state = state, + primaryAction = { + Row( + modifier = + Modifier.fillMaxSize().clickable { + /* Add the primary action */ + coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) } + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete") + if (abs(state.offset) > state.revealThreshold) { + // Delay the text appearance so that it has enough space to be displayed + val textAlpha = + animateFloatAsState( + targetValue = 1f, + animationSpec = tween(durationMillis = 250, delayMillis = 250), + label = "PrimaryActionTextAlpha", + ) + Box(modifier = Modifier.graphicsLayer { alpha = textAlpha.value }) { + Spacer(Modifier.size(5.dp)) + Text("Clear") + } + } + } + }, + undoAction = { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = { + /* Add the undo action */ + coroutineScope.launch { state.animateTo(RevealValue.Covered) } + }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Undo") }, + ) + }, + ) { + Chip( + modifier = + Modifier.fillMaxWidth().semantics { + // Use custom actions to make the primary and secondary actions accessible + customActions = + listOf( + CustomAccessibilityAction("Delete") { + /* Add the primary action click handler */ + true + } + ) + }, + onClick = { /* the click action associated with chip */ }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Swipe Me") }, + ) + } +} + +/** + * A sample on how to use Swipe To Reveal within a list of items, preferably [ScalingLazyColumn]. + */ +@Sampled +@Composable +fun SwipeToRevealWithExpandables() { + // Shape of actions should match with the overlay content. For example, Chips + // should use RoundedCornerShape(CornerSize(percent = 50)), Cards should use + // RoundedCornerShape with appropriate radius, based on the theme. + val actionShape = RoundedCornerShape(corner = CornerSize(percent = 50)) + val itemCount = 10 + val coroutineScope = rememberCoroutineScope() + val expandableStates = List(itemCount) { rememberExpandableState(initiallyExpanded = true) } + ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { + item { ListHeader { Text("Scaling Lazy Column") } } + repeat(itemCount) { current -> + expandableItem(state = expandableStates[current]) { isExpanded -> + val revealState = rememberRevealState() + if (isExpanded) { + SwipeToReveal( + state = revealState, + primaryAction = { + Box( + modifier = + Modifier.fillMaxSize() + .background(Color.Red, actionShape) + .clickable { + coroutineScope.launch { + revealState.animateTo(RevealValue.RightRevealed) + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete", + ) + } + }, + secondaryAction = { + Box( + modifier = + Modifier.fillMaxSize() + .background(Color.Gray, actionShape) + .clickable { /* trigger the optional action */ }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = "More Options", + ) + } + }, + undoAction = { + Chip( + modifier = Modifier.fillMaxWidth(), + onClick = { + coroutineScope.launch { + revealState.animateTo(RevealValue.Covered) + } + }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Undo") }, + ) + }, + onFullSwipe = { + coroutineScope.launch { + delay(1000) + expandableStates[current].expanded = false + } + }, + ) { + Chip( + modifier = + Modifier.fillMaxWidth().semantics { + // Use custom actions to make the primary and secondary actions + // accessible + customActions = + listOf( + CustomAccessibilityAction("Delete") { + /* Add the primary action click handler */ + coroutineScope.launch { + revealState.animateTo(RevealValue.RightRevealed) + } + true + }, + CustomAccessibilityAction("More Options") { + /* Add the secondary action click handler */ + true + }, + ) + }, + onClick = { /* the click action associated with chip */ }, + colors = ChipDefaults.secondaryChipColors(), + label = { Text(text = "Swipe Me") }, + ) + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt new file mode 100644 index 0000000..87e92be --- /dev/null +++ b/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt @@ -0,0 +1,335 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.foundation.samples + +import androidx.annotation.Sampled +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LocalPinnableContainer +import androidx.compose.ui.layout.PinnableContainer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirstOrNull +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults +import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider +import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider.ItemEdge +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults +import androidx.wear.compose.material.Text +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.CardDefaults +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.TitleCard +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight +import kotlin.math.abs +import kotlinx.coroutines.launch + +@Sampled +@Preview +@Composable +fun SimpleTransformingLazyColumnSample() { + val transformationSpec = rememberTransformationSpec() + TransformingLazyColumn(contentPadding = PaddingValues(20.dp)) { + items(count = 10) { index -> + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text(text = "Item $index") + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnWithSnapSample() { + val transformationSpec = rememberTransformationSpec() + val state = rememberTransformingLazyColumnState() + TransformingLazyColumn( + rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state), + flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(state = state), + modifier = Modifier.fillMaxWidth(), + state = state, + contentPadding = PaddingValues(20.dp), + ) { + items(count = 20) { index -> + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text(text = "Item $index") + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnAnimateItemSample() { + val state = rememberTransformingLazyColumnState() + + var list by remember { mutableStateOf(listOf("1", "2", "3")) } + + var next by remember { mutableIntStateOf(4) } + + Box(Modifier.fillMaxSize()) { + TransformingLazyColumn( + state = state, + contentPadding = PaddingValues(5.dp), + modifier = Modifier.background(Color.Black).fillMaxSize(), + ) { + items(list.size, key = { list[it] }) { + Text( + "Item ${list[it]}", + Modifier.animateItem().clickable { + list = list.filter { elem -> elem != list[it] } + }, + ) + } + } + Text( + "+", + Modifier.align(Alignment.CenterStart).padding(horizontal = 5.dp).clickable { + if (list.size < 25) list = list + "${next++}" + }, + ) + Text( + "S", + Modifier.align(Alignment.CenterEnd).padding(horizontal = 5.dp).clickable { + list = list.shuffled() + }, + ) + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnScrollToItemSample() { + val state = + rememberTransformingLazyColumnState( + // Customize initial scroll position of the TransformingLazyColumn. + initialAnchorItemIndex = 10 + ) + val coroutineScope = rememberCoroutineScope() + + TransformingLazyColumn( + modifier = Modifier.background(Color.Black), + state = state, + contentPadding = PaddingValues(vertical = 20.dp), + ) { + items(count = 20) { + Text( + "Item $it", + modifier = + Modifier.drawBehind { + val isCentered = + it == state.anchorItemIndex && + abs(state.anchorItemScrollOffset) < size.height + drawRect(if (isCentered) Color.Green else Color.DarkGray) + } + .padding(5.dp) + .clickable { coroutineScope.launch { state.scrollToItem(it) } }, + ) + } + + item { + Text( + "Scroll to top", + modifier = + Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } }, + ) + } + } + + LaunchedEffect(Unit) { + snapshotFlow { state.anchorItemIndex }.collect { println("Anchor item index: $it") } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnMinimumVerticalContentPaddingSample() { + val transformationSpec = rememberTransformationSpec() + + TransformingLazyColumn(contentPadding = PaddingValues(horizontal = 20.dp)) { + items(count = 20) { index -> + Button( + modifier = + Modifier.fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text(text = "Item $index") + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnFirstLayoutItemProviderSample() { + val state = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + var expandedItemIndex by remember { mutableIntStateOf(-1) } + + // This sample demonstrates how to use the provider API to control the direction of content + // shifting. By default, TransformingLazyColumn uses the center item as the layout reference. + // This means that if an item above the center expands, it pushes content upwards; + // if below, it pushes downwards. + // + // Here, we fix the Bottom/End edge of the clicked item regardless of its position on screen, + // so that when its animated content appears, the card predictably expands *upwards* every time. + val upwardExpandingItemProvider = + remember(state) { + TransformingLazyColumnFirstLayoutItemProvider { centerItem -> + val item = expandedItemIndex + + // Yield to the standard layout behavior during active scrolls. + // This avoids custom layout overhead and ensures the [TransformingLazyColumn] + // tracks the user's scroll gesture using its default center layout reference. + if (item == -1 || state.isScrollInProgress) { + return@TransformingLazyColumnFirstLayoutItemProvider centerItem + } + + // Look up the item's offset from state.layoutInfo (which holds the details + // from the previous measure pass) to maintain its visual position in the current + // pass. + state.layoutInfo.visibleItems + .fastFirstOrNull { visibleItem -> visibleItem.index == item } + ?.let { visibleItem -> + TransformingLazyColumnFirstLayoutItemProvider.ItemInfo( + key = visibleItem.key, + index = visibleItem.index, + // Pin the bottom edge of the item + itemEdge = ItemEdge.End, + // Calculate the exact bottom offset from the previous pass + offset = visibleItem.offset + visibleItem.transformedHeight, + ) + } ?: centerItem + } + } + + TransformingLazyColumn( + state = state, + contentPadding = PaddingValues(horizontal = 20.dp), + firstLayoutItemProvider = upwardExpandingItemProvider, + ) { + items(count = 10, key = { it }) { cardIndex -> + val isExpanded = expandedItemIndex == cardIndex + TitleCard( + onClick = { expandedItemIndex = cardIndex }, + modifier = + Modifier.minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ) + .fillMaxWidth() + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + title = { Text("Card $cardIndex") }, + subtitle = { + AnimatedVisibility(isExpanded) { Text("Expanded content is available here") } + }, + content = { Text("Tap to expand") }, + ) + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnPinnableContainerSample() { + val state = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + + TransformingLazyColumn(state = state, contentPadding = PaddingValues(horizontal = 20.dp)) { + items(count = 20) { index -> + var isPinned by remember { mutableStateOf(false) } + var pinHandle by remember { mutableStateOf(null) } + val pinnableContainer = LocalPinnableContainer.current + + // This state will be reset if the item is scrolled out and not pinned + var counter by remember { mutableIntStateOf(0) } + + TitleCard( + onClick = { counter++ }, + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + title = { Text("Item $index") }, + subtitle = { Text("Count: $counter (Tap card to +)") }, + content = { + // This button toggles the pin state + Button( + onClick = { + if (isPinned) { + pinHandle?.release().also { pinHandle = null } + isPinned = false + } else { + pinHandle = pinnableContainer?.pin() + isPinned = true + } + }, + colors = + if (isPinned) { + ButtonDefaults.buttonColors() + } else { + ButtonDefaults.filledTonalButtonColors() + }, + ) { + Text(if (isPinned) "Unpin" else "Pin Off-screen") + } + }, + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt new file mode 100644 index 0000000..79036c1 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt @@ -0,0 +1,377 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.AlertDialog +import androidx.wear.compose.material3.AlertDialogDefaults +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Sampled +@Composable +@Preview +fun AlertDialogWithConfirmAndDismissSample() { + var showDialog by remember { mutableStateOf(false) } + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + icon = { + Icon( + Icons.Rounded.AccountCircle, + modifier = Modifier.size(32.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("Enable Battery Saver Mode?") }, + text = { Text("Your battery is low. Turn on battery saver.") }, + confirmButton = { + AlertDialogDefaults.ConfirmButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) + }, + dismissButton = { + AlertDialogDefaults.DismissButton( + onClick = { + // Perform dismiss action here + showDialog = false + } + ) + }, + ) { + item { Text(text = "You can configure battery saver mode in the setting menu.") } + item { Button(onClick = {}) { Text(text = "Go to settings") } } + } +} + +@Preview +@Sampled +@Composable +fun AlertDialogWithConfirmAndDismissTransformingContentSample() { + var showDialog by remember { mutableStateOf(false) } + val transformationSpec = rememberTransformationSpec() + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + icon = { + Icon( + Icons.Rounded.AccountCircle, + modifier = Modifier.size(32.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("Enable Battery Saver Mode?") }, + text = { Text("Your battery is low. Turn on battery saver.") }, + transformationSpec = transformationSpec, + confirmButton = { + AlertDialogDefaults.ConfirmButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) + }, + dismissButton = { + AlertDialogDefaults.DismissButton( + onClick = { + // Perform dismiss action here + showDialog = false + } + ) + }, + ) { + item { + Text( + modifier = + Modifier.transformedHeight(this, transformationSpec).graphicsLayer { + with(SurfaceTransformation(transformationSpec)) { + applyContentTransformation() + applyContainerTransformation() + } + }, + text = "You can configure battery saver mode in the setting menu.", + ) + } + item { + Button( + modifier = Modifier.transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text(text = "Go to settings") + } + } + } +} + +@Preview +@Sampled +@Composable +fun AlertDialogWithEdgeButtonSample() { + var showDialog by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + icon = { + Icon( + Icons.Rounded.AccountCircle, + modifier = Modifier.size(32.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("Mobile network is not currently available") }, + text = { Text("Please try again later or check your network settings.") }, + edgeButton = { + AlertDialogDefaults.EdgeButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) + }, + ) +} + +@Preview +@Sampled +@Composable +fun AlertDialogWithEdgeButtonTransformingContentSample() { + var showDialog by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + icon = { + Icon( + Icons.Rounded.AccountCircle, + modifier = Modifier.size(32.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("Mobile network is not currently available") }, + text = { Text("Please try again later or check your network settings.") }, + transformationSpec = rememberTransformationSpec(), + edgeButton = { + AlertDialogDefaults.EdgeButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) + }, + ) +} + +@Preview +@Sampled +@Composable +fun AlertDialogWithContentGroupsSample() { + var showDialog by remember { mutableStateOf(false) } + var weatherEnabled by remember { mutableStateOf(false) } + var calendarEnabled by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + title = { Text("Share your location") }, + text = { Text(" The following apps have asked you to share your location") }, + edgeButton = { + AlertDialogDefaults.EdgeButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) { + Text("Share once") + } + }, + ) { + item { + SwitchButton( + modifier = Modifier.fillMaxWidth(), + checked = weatherEnabled, + onCheckedChange = { weatherEnabled = it }, + label = { Text("Weather") }, + ) + } + item { + SwitchButton( + modifier = Modifier.fillMaxWidth(), + checked = calendarEnabled, + onCheckedChange = { calendarEnabled = it }, + label = { Text("Calendar") }, + ) + } + item { AlertDialogDefaults.GroupSeparator() } + item { + FilledTonalButton( + modifier = Modifier.fillMaxWidth(), + onClick = {}, + label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") }, + ) + } + item { + FilledTonalButton( + modifier = Modifier.fillMaxWidth(), + onClick = {}, + label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") }, + ) + } + } +} + +@Preview +@Sampled +@Composable +fun AlertDialogWithContentGroupsTransformingContentSample() { + var showDialog by remember { mutableStateOf(false) } + var weatherEnabled by remember { mutableStateOf(false) } + var calendarEnabled by remember { mutableStateOf(false) } + val transformationSpec = rememberTransformationSpec() + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showDialog = true }, + label = { Text("Show Dialog") }, + ) + } + AlertDialog( + visible = showDialog, + onDismissRequest = { showDialog = false }, + title = { Text("Share your location") }, + text = { Text(" The following apps have asked you to share your location") }, + transformationSpec = transformationSpec, + edgeButton = { + AlertDialogDefaults.EdgeButton( + onClick = { + // Perform confirm action here + showDialog = false + } + ) { + Text("Share once") + } + }, + ) { + item { + SwitchButton( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + checked = weatherEnabled, + onCheckedChange = { weatherEnabled = it }, + label = { Text("Weather") }, + transformation = SurfaceTransformation(transformationSpec), + ) + } + item { + SwitchButton( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + checked = calendarEnabled, + onCheckedChange = { calendarEnabled = it }, + label = { Text("Calendar") }, + transformation = SurfaceTransformation(transformationSpec), + ) + } + item { AlertDialogDefaults.GroupSeparator() } + item { + FilledTonalButton( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + onClick = {}, + label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") }, + transformation = SurfaceTransformation(transformationSpec), + ) + } + item { + FilledTonalButton( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + onClick = {}, + label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") }, + transformation = SurfaceTransformation(transformationSpec), + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt b/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt new file mode 100644 index 0000000..4cd8d4a --- /dev/null +++ b/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt @@ -0,0 +1,165 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material3.AnimatedText +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.rememberAnimatedTextFontRegistry +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun AnimatedTextSample() { + val scope = rememberCoroutineScope() + val animatable = remember { Animatable(0f) } + val animate = { + scope.launch { + // Animate from 0 to 1 and then back to 0. + animatable.animateTo(1f) + animatable.animateTo(0f) + } + } + val animatedTextFontRegistry = + rememberAnimatedTextFontRegistry( + // Variation axes at the start of the animation, width 10, weight 200 + startFontVariationSettings = + FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)), + // Variation axes at the end of the animation, width 100, weight 500 + endFontVariationSettings = + FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)), + startFontSize = 30.sp, + endFontSize = 40.sp, + ) + AnimatedText( + text = "Hello!", + fontRegistry = animatedTextFontRegistry, + // Content alignment anchors the animation at the vertical center, expanding horizontally + contentAlignment = Alignment.CenterStart, + progressFraction = { animatable.value }, + modifier = Modifier.clickable(onClick = { animate() }), + ) + LaunchedEffect(Unit) { animate() } +} + +@Sampled +@Composable +fun AnimatedTextSampleButtonResponse() { + val scope = rememberCoroutineScope() + val animatedTextFontRegistry = + rememberAnimatedTextFontRegistry( + // Variation axes at the start of the animation, width 10, weight 200 + startFontVariationSettings = + FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)), + // Variation axes at the end of the animation, width 100, weight 500 + endFontVariationSettings = + FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)), + startFontSize = 30.sp, + endFontSize = 30.sp, + ) + val number = remember { mutableIntStateOf(0) } + val textAnimatable = remember { Animatable(0f) } + Row(verticalAlignment = Alignment.CenterVertically) { + Button( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = { + number.value -= 1 + scope.launch { + textAnimatable.animateTo(1f) + textAnimatable.animateTo(0f) + } + }, + label = { + Text(modifier = Modifier.semantics { contentDescription = "Decrease" }, text = "-") + }, + ) + AnimatedText( + text = "${number.value}", + fontRegistry = animatedTextFontRegistry, + progressFraction = { textAnimatable.value }, + ) + Button( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = { + number.value += 1 + scope.launch { + textAnimatable.animateTo(1f) + textAnimatable.animateTo(0f) + } + }, + label = { + Text(modifier = Modifier.semantics { contentDescription = "Increase" }, text = "+") + }, + ) + } +} + +@Sampled +@Composable +fun AnimatedTextSampleSharedFontRegistry() { + val animatedTextFontRegistry = + rememberAnimatedTextFontRegistry( + // Variation axes at the start of the animation, width 50, weight 300 + startFontVariationSettings = + FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)), + // Variation axes at the end of the animation are the same as the start axes + endFontVariationSettings = + FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)), + startFontSize = 15.sp, + endFontSize = 25.sp, + ) + val firstAnimatable = remember { Animatable(0f) } + val secondAnimatable = remember { Animatable(0f) } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + AnimatedText( + text = "Top Text", + fontRegistry = animatedTextFontRegistry, + progressFraction = { firstAnimatable.value }, + ) + AnimatedText( + text = "Bottom Text", + fontRegistry = animatedTextFontRegistry, + progressFraction = { secondAnimatable.value }, + ) + } + LaunchedEffect(Unit) { + firstAnimatable.animateTo(1f) + firstAnimatable.animateTo(0f) + secondAnimatable.animateTo(1f) + secondAnimatable.animateTo(0f) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt new file mode 100644 index 0000000..b29826a --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt @@ -0,0 +1,91 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonGroup +import androidx.wear.compose.material3.Text + +@Sampled +@Composable +fun ButtonGroupSample() { + val interactionSource1 = remember { MutableInteractionSource() } + val interactionSource2 = remember { MutableInteractionSource() } + + Box(contentAlignment = Alignment.Center) { + ButtonGroup(Modifier.fillMaxWidth()) { + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource1), + interactionSource = interactionSource1, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("L") } + } + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource2), + interactionSource = interactionSource2, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("R") } + } + } + } +} + +@Sampled +@Composable +fun ButtonGroupThreeButtonsSample() { + val interactionSource1 = remember { MutableInteractionSource() } + val interactionSource2 = remember { MutableInteractionSource() } + val interactionSource3 = remember { MutableInteractionSource() } + + Box(contentAlignment = Alignment.Center) { + ButtonGroup(Modifier.fillMaxWidth()) { + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource1), + interactionSource = interactionSource1, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("A") } + } + Button( + onClick = {}, + modifier = Modifier.weight(1.5f).animateWidth(interactionSource2), + interactionSource = interactionSource2, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("B") } + } + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource3), + interactionSource = interactionSource3, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("C") } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt new file mode 100644 index 0000000..5a3c305 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt @@ -0,0 +1,496 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ChildButton +import androidx.wear.compose.material3.CompactButton +import androidx.wear.compose.material3.CompactButtonDefaults +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.OutlinedButton +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState +import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture +import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun SimpleButtonSample(modifier: Modifier = Modifier) { + Button(onClick = { /* Do something */ }, label = { Text("Simple Button") }, modifier = modifier) +} + +@Sampled +@Composable +fun ButtonSample() { + Button( + onClick = { /* Do something */ }, + label = { Text("Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun ButtonLargeIconSample(enabled: Boolean = true) { + // When customising the icon size, it is recommended to also specify + // the associated content padding + Button( + onClick = { /* Do something */ }, + enabled = enabled, + label = { Text("Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.LargeIconSize), + ) + }, + contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun ButtonExtraLargeIconSample(enabled: Boolean = true) { + // When customising the icon size, it is recommended to also specify + // the associated content padding + Button( + onClick = { /* Do something */ }, + enabled = enabled, + label = { Text("Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.ExtraLargeIconSize), + ) + }, + contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun ButtonWithImageSample(enabled: Boolean = true) { + Button( + onClick = { /* Do something */ }, + containerPainter = + ButtonDefaults.containerPainter( + image = painterResource(id = R.drawable.backgroundimage) + ), + enabled = enabled, + label = { Text("Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun SimpleFilledTonalButtonSample() { + FilledTonalButton( + onClick = { /* Do something */ }, + label = { Text("Filled Tonal Button") }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun FilledTonalButtonSample() { + FilledTonalButton( + onClick = { /* Do something */ }, + label = { Text("Filled Tonal Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun SimpleFilledVariantButtonSample() { + Button( + onClick = { /* Do something */ }, + colors = ButtonDefaults.filledVariantButtonColors(), + label = { Text("Filled Variant Button") }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun FilledVariantButtonSample() { + Button( + onClick = { /* Do something */ }, + colors = ButtonDefaults.filledVariantButtonColors(), + label = { Text("Filled Variant Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun SimpleOutlinedButtonSample() { + OutlinedButton( + onClick = { /* Do something */ }, + label = { Text("Outlined Button") }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun OutlinedButtonSample() { + OutlinedButton( + onClick = { /* Do something */ }, + label = { Text("Outlined Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun SimpleChildButtonSample() { + ChildButton( + onClick = { /* Do something */ }, + label = { + Text("Child Button", textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun ChildButtonSample() { + ChildButton( + onClick = { /* Do something */ }, + label = { Text("Child Button") }, + secondaryLabel = { Text("Secondary label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Sampled +@Composable +fun CompactButtonSample(modifier: Modifier = Modifier) { + CompactButton( + onClick = { /* Do something */ }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), + ) + }, + modifier = modifier, + label = { Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis) }, + ) +} + +@Sampled +@Composable +fun CompactButtonWithContentSample(modifier: Modifier = Modifier) { + CompactButton( + onClick = { /* Do something */ }, + modifier = modifier, + content = { + Box( + Modifier.size(CompactButtonDefaults.ExtraSmallIconSize) + .clip(CircleShape) + .background(Color.Green) + ) + Spacer(Modifier.width(ButtonDefaults.IconSpacing)) + Text( + "Custom content", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Italic, + letterSpacing = 0.5.sp, + ) + }, + ) +} + +@Sampled +@Composable +fun CompactButtonWithOnLongClickSample( + onClickHandler: () -> Unit, + onLongClickHandler: () -> Unit, + modifier: Modifier = Modifier, +) { + CompactButton( + onClick = onClickHandler, + onLongClick = onLongClickHandler, + onLongClickLabel = "Long click", + label = { Text("Long clickable") }, + modifier = + modifier.semantics { + // Also override the 'click label' to say 'Double tap to press' instead of + // the usual 'Double tap to activate'. + onClick("press") { false } + }, + ) +} + +@Sampled +@Composable +fun FilledTonalCompactButtonSample(modifier: Modifier = Modifier) { + CompactButton( + onClick = { /* Do something */ }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), + ) + }, + colors = ButtonDefaults.filledTonalButtonColors(), + modifier = modifier, + label = { + Text("Filled Tonal Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + ) +} + +@Sampled +@Composable +fun OutlinedCompactButtonSample(modifier: Modifier = Modifier) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + var expanded by remember { mutableStateOf(false) } + if (expanded) { + Text("A multiline string showing two lines") + } else { + Text("One line text") + } + Spacer(Modifier.height(ButtonDefaults.IconSpacing)) + CompactButton( + onClick = { expanded = !expanded }, + colors = ButtonDefaults.outlinedButtonColors(), + border = ButtonDefaults.outlinedButtonBorder(enabled = true), + modifier = modifier, + content = { + if (expanded) { + Text("Show Less", maxLines = 1, overflow = TextOverflow.Ellipsis) + } else { + Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis) + } + Spacer(Modifier.width(ButtonDefaults.IconSpacing)) + if (expanded) { + Icon( + Icons.Filled.KeyboardArrowUp, + contentDescription = "Collapse", + modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), + ) + } else { + Icon( + Icons.Filled.KeyboardArrowDown, + contentDescription = "Expand", + modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), + ) + } + }, + ) + } +} + +@Sampled +@Composable +fun ButtonContentWithOneHandedGestureSample() { + var label by remember { mutableStateOf("Filled Button") } + val onClick = remember { { label = "Gestured" } } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "click", + interactionSource = interactionSource, + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + onGesture = onClick, + ), + content = { + OneHandedGestureClickIndicator( + gestureConfiguration = gestureConfig, + state = indicatorState, + gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary, + ) { + ButtonDefaults.Content( + secondaryLabel = { Text("Secondary Label") }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + colors = ButtonDefaults.buttonColors(), + label = { Text(label) }, + ) + } + }, + ) + } +} + +@Sampled +@Composable +fun CompactButtonContentWithOneHandedGestureSample() { + var label by remember { mutableStateOf("Compact Button") } + val onClick = remember { { label = "Gestured" } } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CompactButton( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "click", + interactionSource = interactionSource, + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + onGesture = onClick, + ), + content = { + OneHandedGestureClickIndicator( + gestureConfiguration = gestureConfig, + state = indicatorState, + gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary, + ) { + CompactButtonDefaults.Content( + icon = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), + ) + }, + colors = ButtonDefaults.buttonColors(), + label = { Text(label) }, + ) + } + }, + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt b/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt new file mode 100644 index 0000000..bcf5d47 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt @@ -0,0 +1,470 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.AppCard +import androidx.wear.compose.material3.Card +import androidx.wear.compose.material3.CardDefaults +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.OutlinedCard +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TitleCard +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState +import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture +import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration +import kotlinx.coroutines.launch + +@Preview +@Sampled +@Composable +fun CardSample() { + Card(onClick = { /* Do something */ }) { Text("Card") } +} + +@Sampled +@Composable +fun NonClickableCardSample() { + Card { Text("Non Clickable Card") } +} + +@Sampled +@Composable +fun CardWithOnLongClickSample(onLongClickHandler: () -> Unit) { + Card( + onClick = { /* Do something */ }, + onLongClick = onLongClickHandler, + onLongClickLabel = "Long click", + ) { + Text("Card with long click") + } +} + +@Sampled +@Composable +fun AppCardSample() { + AppCard( + onClick = { /* Do something */ }, + appName = { Text("App name") }, + title = { Text("Card title") }, + time = { Text("Now") }, + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun NonClickableAppCardSample() { + AppCard( + appName = { Text("App name") }, + title = { Text("Card title") }, + time = { Text("Now") }, + ) { + Text("Non clickable card content") + } +} + +@Sampled +@Composable +fun AppCardWithIconSample() { + AppCard( + onClick = { /* Do something */ }, + appName = { Text("App name") }, + appImage = { + Icon( + painter = painterResource(id = android.R.drawable.star_big_off), + contentDescription = "Star icon", + modifier = + Modifier.size(CardDefaults.AppImageSize) + .wrapContentSize(align = Alignment.Center), + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("Card title") }, + time = { Text("Now") }, + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun AppCardWithImageSample() { + val configuration = LocalConfiguration.current + // Add padding to the end of the image in order to maintain the correct proportions + // between the image and the card. + val imageEndPaddingDp = (0.15f * configuration.screenWidthDp).dp + AppCard( + onClick = { /* Do something */ }, + appName = { Text("App name") }, + appImage = { + Icon( + painter = painterResource(id = android.R.drawable.star_big_off), + contentDescription = "Star icon", + modifier = + Modifier.size(CardDefaults.AppImageSize) + .wrapContentSize(align = Alignment.Center), + tint = MaterialTheme.colorScheme.primary, + ) + }, + title = { Text("With image") }, + time = { Text("Now") }, + ) { + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth()) { + Image( + modifier = + Modifier.weight(1f).aspectRatio(16f / 9f).clip(RoundedCornerShape(16.dp)), + painter = painterResource(id = R.drawable.card_content_image), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(imageEndPaddingDp)) + } + } +} + +@Sampled +@Composable +fun TitleCardSample() { + TitleCard( + onClick = { /* Do something */ }, + title = { Text("Title card") }, + time = { Text("Now") }, + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun NonClickableTitleCardSample() { + TitleCard(title = { Text("Title card") }, time = { Text("Now") }) { + Text("Non clickable Card content") + } +} + +@Sampled +@Composable +fun TitleCardWithSubtitleAndTimeSample() { + TitleCard( + onClick = { /* Do something */ }, + time = { Text("Now") }, + title = { Text("Title card") }, + subtitle = { Text("Subtitle") }, + ) +} + +@Preview +@Sampled +@Composable +fun TitleCardWithMultipleImagesSample() { + TitleCard( + onClick = { /* Do something */ }, + title = { Text("Title card") }, + time = { Text("Now") }, + modifier = Modifier.semantics { contentDescription = "Background image" }, + ) { + Spacer(Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth()) { + Image( + modifier = + Modifier.weight(2f) + .height(68.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(16.dp)), + painter = painterResource(id = R.drawable.card_content_image), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + Spacer(Modifier.width(4.dp)) + Image( + modifier = + Modifier.weight(1f) + .height(68.dp) + .align(Alignment.CenterVertically) + .clip(RoundedCornerShape(16.dp)), + painter = painterResource(id = R.drawable.card_content_image), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } + } +} + +@Sampled +@Composable +fun TitleCardWithImageWithTimeAndTitleSample() { + TitleCard( + onClick = { /* Do something */ }, + containerPainter = + CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), + title = { Text("Card title") }, + subtitle = { Text("Subtitle") }, + time = { Text("Now") }, + contentPadding = CardDefaults.CardWithContainerPainterContentPadding, + modifier = Modifier.semantics { contentDescription = "Background image" }, + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun NonClickableTitleCardWithImageWithTimeAndTitleSample() { + TitleCard( + containerPainter = + CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), + title = { Text("Card title") }, + subtitle = { Text("Subtitle") }, + time = { Text("Now") }, + contentPadding = CardDefaults.CardWithContainerPainterContentPadding, + modifier = Modifier.semantics { contentDescription = "Background image" }, + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun OutlinedCardSample() { + OutlinedCard(onClick = { /* Do something */ }) { Text("Outlined card") } +} + +@Sampled +@Composable +fun NonClickableOutlinedCardSample() { + OutlinedCard { Text("Non-clickable outlined card") } +} + +@Sampled +@Composable +fun ImageCardSample() { + Card( + onClick = { /* Do something */ }, + containerPainter = + CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), + ) { + Text("Image card") + } +} + +@Sampled +@Composable +fun NonClickableImageCardSample() { + Card( + containerPainter = + CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)) + ) { + Text("Non clickable image card") + } +} + +@Sampled +@Composable +fun OutlinedAppCardSample() { + AppCard( + onClick = { /* Do something */ }, + appName = { Text("App name") }, + appImage = { + Icon( + Icons.Filled.Favorite, + contentDescription = "Favorite icon", + modifier = Modifier.size(CardDefaults.AppImageSize), + ) + }, + title = { Text("App card") }, + time = { Text("Now") }, + colors = CardDefaults.outlinedCardColors(), + border = CardDefaults.outlinedCardBorder(), + ) { + Text("Card content") + } +} + +@Sampled +@Composable +fun OutlinedTitleCardSample() { + TitleCard( + onClick = { /* Do something */ }, + title = { Text("Title card") }, + time = { Text("Now") }, + colors = CardDefaults.outlinedCardColors(), + border = CardDefaults.outlinedCardBorder(), + ) { + Text("Card content") + } +} + +@Sampled +@Preview +@Composable +fun CardFillContentSample() { + Card( + onClick = { /* Do something */ }, + // Constrains the card to fill background up to the intrinsic height. + modifier = Modifier.height(IntrinsicSize.Min), + ) { + Text( + "Filled Content", + color = MaterialTheme.colorScheme.onPrimary, + modifier = + Modifier.fillMaxHeight() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary) + .wrapContentSize(Alignment.Center), + ) + } +} + +@Sampled +@Composable +fun AppCardContentWithOneHandedGestureSample() { + var label by remember { mutableStateOf("App Card") } + val onClick = remember { { label = "Gestured" } } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Card( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.padding(horizontal = 12.dp) + .fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "click", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + interactionSource = interactionSource, + onGesture = onClick, + ), + ) { + OneHandedGestureClickIndicator( + gestureConfiguration = gestureConfig, + state = indicatorState, + gestureIndicatorTint = MaterialTheme.colorScheme.onSurface, + ) { + CardDefaults.AppCardContent( + appName = { Text("App Name") }, + title = { Text(label) }, + appImage = { + Icon( + painter = painterResource(R.drawable.ic_favorite_rounded), + contentDescription = "Favorite icon", + modifier = Modifier.size(CardDefaults.AppImageSize), + ) + }, + time = { Text("now") }, + ) { + Text("Card body") + } + } + } + } +} + +@Sampled +@Composable +fun TitleCardContentWithOneHandedGestureSample() { + var label by remember { mutableStateOf("Title Card") } + val onClick = remember { { label = "Gestured" } } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Card( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.padding(horizontal = 12.dp) + .fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "click", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + interactionSource = interactionSource, + onGesture = onClick, + ), + ) { + OneHandedGestureClickIndicator( + gestureConfiguration = gestureConfig, + state = indicatorState, + gestureIndicatorTint = MaterialTheme.colorScheme.onSurface, + ) { + CardDefaults.TitleCardContent( + title = { Text(label) }, + time = { Text("now") }, + subtitle = { Text("Subtitle") }, + ) { + Text("Card body") + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt new file mode 100644 index 0000000..3ae73bd --- /dev/null +++ b/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt @@ -0,0 +1,69 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.material3.CheckboxButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.SplitCheckboxButton +import androidx.wear.compose.material3.Text + +@Sampled +@Preview +@Composable +fun CheckboxButtonSample() { + var checked by remember { mutableStateOf(true) } + CheckboxButton( + label = { Text("Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + secondaryLabel = { + Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + checked = checked, + onCheckedChange = { checked = it }, + icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, + enabled = true, + ) +} + +@Sampled +@Preview +@Composable +fun SplitCheckboxButtonSample() { + var checked by remember { mutableStateOf(true) } + SplitCheckboxButton( + label = { Text("Split Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + checked = checked, + onCheckedChange = { checked = it }, + toggleContentDescription = "Split Checkbox Button Sample", + onContainerClick = { + /* Do something */ + }, + containerClickLabel = "click", + enabled = true, + ) +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt new file mode 100644 index 0000000..1f31892 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt @@ -0,0 +1,162 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.ConfirmationDialog +import androidx.wear.compose.material3.ConfirmationDialogDefaults +import androidx.wear.compose.material3.FailureConfirmationDialog +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.SuccessConfirmationDialog +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.confirmationDialogCurvedText +import androidx.wear.compose.material3.samples.icons.FavoriteIcon + +@Sampled +@Composable +fun ConfirmationDialogSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Show Confirmation") }, + ) + } + + // Has an icon and a short curved text content, which will be displayed along the bottom edge of + // the screen. + val curvedTextStyle = ConfirmationDialogDefaults.curvedTextStyle + ConfirmationDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + curvedText = { confirmationDialogCurvedText("Confirmed", curvedTextStyle) }, + ) { + FavoriteIcon(ConfirmationDialogDefaults.IconSize) + } +} + +@Sampled +@Composable +fun LongTextConfirmationDialogSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Show Confirmation") }, + ) + } + + // Has an icon and a text content. Text will be displayed in the center of the screen below the + // icon. + ConfirmationDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + text = { Text(text = "Your message has been sent") }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = null, + modifier = Modifier.size(ConfirmationDialogDefaults.SmallIconSize), + ) + } +} + +@Sampled +@Composable +fun FailureConfirmationDialogSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Show Confirmation") }, + ) + } + + val text = "Failure" + val style = ConfirmationDialogDefaults.curvedTextStyle + FailureConfirmationDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + curvedText = { confirmationDialogCurvedText(text, style) }, + ) +} + +@Sampled +@Composable +fun FailureConfirmationDialogWithGenericFailureIconSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Show Confirmation") }, + ) + } + + val text = "Failure" + val style = ConfirmationDialogDefaults.curvedTextStyle + FailureConfirmationDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + curvedText = { confirmationDialogCurvedText(text, style) }, + content = { ConfirmationDialogDefaults.GenericFailureIcon() }, + ) +} + +@Sampled +@Composable +fun SuccessConfirmationDialogSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Show Confirmation") }, + ) + } + + val text = "Success" + val style = ConfirmationDialogDefaults.curvedTextStyle + SuccessConfirmationDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + curvedText = { confirmationDialogCurvedText(text, style) }, + ) +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt b/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt new file mode 100644 index 0000000..4e1a9ae --- /dev/null +++ b/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt @@ -0,0 +1,78 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap.Companion.Round +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.CurvedDirection +import androidx.wear.compose.foundation.CurvedLayout +import androidx.wear.compose.foundation.CurvedModifier +import androidx.wear.compose.foundation.angularSizeDp +import androidx.wear.compose.foundation.background +import androidx.wear.compose.foundation.curvedBox +import androidx.wear.compose.foundation.curvedComposable +import androidx.wear.compose.foundation.curvedRow +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.CurvedTextDefaults +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.curvedText + +@Sampled +@Composable +fun CurvedTextTop() { + val backgroundColor = MaterialTheme.colorScheme.onPrimary + val customColor = MaterialTheme.colorScheme.tertiaryDim + CurvedLayout { + curvedRow(CurvedModifier.background(backgroundColor, Round)) { + curvedText("Calling", color = customColor) + curvedBox(CurvedModifier.angularSizeDp(5.dp)) {} + curvedText("Camilia Garcia") + } + } +} + +@Sampled +@Composable +fun CurvedTextBottom() { + val backgroundColor = MaterialTheme.colorScheme.onPrimary + CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) { + curvedRow(CurvedModifier.background(backgroundColor, Round)) { + curvedComposable { + Icon( + Icons.Filled.Warning, + contentDescription = "Warning", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + } + curvedText( + "Network lost", + maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle, + overflow = TextOverflow.Ellipsis, + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt b/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt new file mode 100644 index 0000000..261e5d7 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt @@ -0,0 +1,131 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.DatePicker +import androidx.wear.compose.material3.DatePickerType +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.Text +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Sampled +@Composable +fun DatePickerSample() { + var showDatePicker by remember { mutableStateOf(true) } + var datePickerDate by remember { mutableStateOf(LocalDate.now()) } + val formatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + .withLocale(LocalConfiguration.current.locales[0]) + if (showDatePicker) { + DatePicker( + initialDate = datePickerDate, // Initialize with last picked date on reopen + onDatePicked = { + datePickerDate = it + showDatePicker = false + }, + ) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = { showDatePicker = true }, + label = { Text("Selected Date") }, + secondaryLabel = { Text(datePickerDate.format(formatter)) }, + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, + ) + } + } +} + +@Sampled +@Composable +fun DatePickerYearMonthDaySample() { + var showDatePicker by remember { mutableStateOf(true) } + var datePickerDate by remember { mutableStateOf(LocalDate.now()) } + + val formatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + .withLocale(LocalConfiguration.current.locales[0]) + + if (showDatePicker) { + DatePicker( + initialDate = datePickerDate, // Initialize with last picked date on reopen + onDatePicked = { + datePickerDate = it + showDatePicker = false + }, + datePickerType = DatePickerType.YearMonthDay, + ) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = { showDatePicker = true }, + label = { Text("Selected Date") }, + secondaryLabel = { Text(datePickerDate.format(formatter)) }, + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, + ) + } + } +} + +@Sampled +@Composable +fun DatePickerFutureOnlySample() { + val currentDate = LocalDate.now() + var showDatePicker by remember { mutableStateOf(true) } + var datePickerDate by remember { mutableStateOf(LocalDate.now()) } + val formatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + .withLocale(LocalConfiguration.current.locales[0]) + if (showDatePicker) { + DatePicker( + initialDate = datePickerDate, // Initialize with last picked date on reopen + onDatePicked = { + datePickerDate = it + showDatePicker = false + }, + datePickerType = DatePickerType.YearMonthDay, + minValidDate = currentDate, + ) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = { showDatePicker = true }, + label = { Text("Selected Date") }, + secondaryLabel = { Text(datePickerDate.format(formatter)) }, + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt new file mode 100644 index 0000000..45d4651 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt @@ -0,0 +1,74 @@ +``` +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ColorScheme +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.dynamicColorScheme +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Sampled +@Composable +fun DynamicColorSchemeSample() { + val dynamicColorScheme = dynamicColorScheme(LocalContext.current) + val transformationSpec = rememberTransformationSpec() + + // Fallback to the default color scheme if dynamic colors are unavailable + MaterialTheme(colorScheme = dynamicColorScheme ?: ColorScheme()) { + val hasDynamicColors = dynamicColorScheme != null + TransformingLazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(20.dp), + ) { + if (!hasDynamicColors) { + item { Text("Dynamic color is not available.") } + } + items(5) { index -> + // The button's defaults will pick up the dynamic primary color from the + // MaterialTheme + Button( + label = { Text("Primary Button ${index + 1}") }, + modifier = + Modifier.fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ), + onClick = {}, + transformation = SurfaceTransformation(transformationSpec), + ) + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt new file mode 100644 index 0000000..4a01bc8 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt @@ -0,0 +1,63 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.EdgeButton +import androidx.wear.compose.material3.EdgeButtonSize +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.Text + +@Sampled +@Composable +fun EdgeButtonSample() { + Column( + Modifier.fillMaxSize().padding(horizontal = 20.dp).padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) { + Text( + "EdgeButton hugs the bottom of the curved screen", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + } + EdgeButton(onClick = { /* Do something */ }, buttonSize = EdgeButtonSize.Medium) { + Icon( + Icons.Filled.Check, + contentDescription = "Check icon", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt b/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt new file mode 100644 index 0000000..4605ac0 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt @@ -0,0 +1,60 @@ +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.FadingExpandingLabel + +@Sampled +@Composable +fun FadingExpandingLabelButtonSample() { + var text by remember { mutableStateOf("Line of Text One.") } + var lines by remember { mutableIntStateOf(1) } + + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = { + lines = lines % 3 + 1 + text = + (1..lines).joinToString(separator = "\n") { + when (it) { + 1 -> "Line of Text One." + 2 -> "Line of Text Two." + else -> "Line of Text Three." + } + } + }, + modifier = Modifier.fillMaxWidth(), + label = { FadingExpandingLabel(text = text, textAlign = TextAlign.Left) }, + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt new file mode 100644 index 0000000..7ecc104 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt @@ -0,0 +1,125 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.Image +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.wear.compose.material3.FilledIconButton +import androidx.wear.compose.material3.FilledTonalIconButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.IconButton +import androidx.wear.compose.material3.IconButtonColors +import androidx.wear.compose.material3.IconButtonDefaults +import androidx.wear.compose.material3.IconButtonShapes +import androidx.wear.compose.material3.OutlinedIconButton + +@Composable +@Sampled +fun IconButtonSample() { + IconButton(onClick = { /* Do something */ }) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun FilledIconButtonSample() { + FilledIconButton(onClick = { /* Do something */ }) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun FilledVariantIconButtonSample() { + FilledIconButton( + onClick = { /* Do something */ }, + colors = IconButtonDefaults.filledVariantIconButtonColors(), + ) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun FilledTonalIconButtonSample() { + FilledTonalIconButton(onClick = { /* Do something */ }) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun OutlinedIconButtonSample() { + OutlinedIconButton(onClick = { /* Do something */ }) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Sampled +@Composable +fun IconButtonWithOnLongClickSample(onLongClick: () -> Unit) { + IconButton( + onClick = { /* Do something for onClick*/ }, + onLongClick = onLongClick, + onLongClickLabel = "Long click", + ) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun IconButtonWithCornerAnimationSample( + colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors() +) { + FilledIconButton( + onClick = { /* Do something */ }, + shapes = IconButtonDefaults.animatedShapes(), + colors = colors, + ) { + Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") + } +} + +@Composable +@Sampled +fun IconButtonWithImageSample( + painter: Painter = painterResource(R.drawable.card_content_image), + enabled: Boolean = true, + shapes: IconButtonShapes = IconButtonDefaults.shapes(), +) { + IconButton(onClick = { /* Do something */ }, shapes = shapes, enabled = enabled) { + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = + if (enabled) Modifier else Modifier.alpha(IconButtonDefaults.DisabledImageOpacity), + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt new file mode 100644 index 0000000..23c16da --- /dev/null +++ b/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt @@ -0,0 +1,116 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.IconToggleButton +import androidx.wear.compose.material3.IconToggleButtonDefaults +import androidx.wear.compose.material3.samples.icons.WifiOffIcon +import androidx.wear.compose.material3.samples.icons.WifiOnIcon + +@Sampled +@Composable +fun IconToggleButtonSample() { + var firstChecked by remember { mutableStateOf(true) } + var secondChecked by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + IconToggleButton( + checked = firstChecked, + onCheckedChange = { firstChecked = !firstChecked }, + shapes = IconToggleButtonDefaults.animatedShapes(), + ) { + if (firstChecked) { + WifiOnIcon() + } else { + WifiOffIcon() + } + } + + Spacer(modifier = Modifier.width(5.dp)) + + IconToggleButton( + checked = secondChecked, + onCheckedChange = { secondChecked = !secondChecked }, + shapes = IconToggleButtonDefaults.animatedShapes(), + ) { + if (secondChecked) { + WifiOnIcon() + } else { + WifiOffIcon() + } + } + } +} + +@Sampled +@Composable +fun IconToggleButtonVariantSample() { + var firstChecked by remember { mutableStateOf(true) } + var secondChecked by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + IconToggleButton( + checked = firstChecked, + onCheckedChange = { firstChecked = !firstChecked }, + shapes = IconToggleButtonDefaults.variantAnimatedShapes(), + ) { + if (firstChecked) { + WifiOnIcon() + } else { + WifiOffIcon() + } + } + + Spacer(modifier = Modifier.width(5.dp)) + + IconToggleButton( + checked = secondChecked, + onCheckedChange = { secondChecked = !secondChecked }, + shapes = IconToggleButtonDefaults.variantAnimatedShapes(), + ) { + if (secondChecked) { + WifiOnIcon() + } else { + WifiOffIcon() + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt new file mode 100644 index 0000000..7f02be2 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt @@ -0,0 +1,68 @@ +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.IconButton +import androidx.wear.compose.material3.LevelIndicator +import androidx.wear.compose.material3.StepperDefaults +import androidx.wear.compose.material3.samples.icons.VolumeDownIcon +import androidx.wear.compose.material3.samples.icons.VolumeUpIcon + +@Sampled +@Composable +fun LevelIndicatorSample() { + var value by remember { mutableFloatStateOf(0.5f) } + Box(modifier = Modifier.fillMaxSize()) { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + IconButton( + modifier = Modifier.padding(horizontal = 16.dp), + enabled = value < 1f, + onClick = { value += 0.1f }, + ) { + VolumeUpIcon(StepperDefaults.IconSize) + } + IconButton( + modifier = Modifier.padding(horizontal = 16.dp), + enabled = value > 0f, + onClick = { value -= 0.1f }, + ) { + VolumeDownIcon(StepperDefaults.IconSize) + } + } + LevelIndicator(value = { value }, modifier = Modifier.align(Alignment.CenterStart)) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt new file mode 100644 index 0000000..8256973 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt @@ -0,0 +1,177 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults +import androidx.wear.compose.material3.ListSubHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Sampled +@Preview +@Composable +fun ListHeaderSample() { + val transformationSpec = rememberTransformationSpec() + val scrollState = rememberTransformingLazyColumnState() + + AppScaffold { + ScreenScaffold(scrollState = scrollState) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = contentPadding, + ) { + item { + ListHeader( + modifier = + Modifier.minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Settings") + } + } + item { + ListSubHeader( + modifier = + Modifier.minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + icon = { + Icon( + painter = painterResource(R.drawable.ic_connectivity), + contentDescription = "Connectivity", + ) + }, + label = { Text("Connectivity") }, + ) + } + item { + Button( + modifier = + Modifier.fillMaxWidth() + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + icon = { + Icon( + painter = painterResource(R.drawable.ic_bluetooth), + contentDescription = "Bluetooth", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + ) { + Text("Bluetooth") + } + } + item { + Button( + modifier = + Modifier.fillMaxWidth() + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + icon = { + Icon( + painter = painterResource(R.drawable.ic_wifi), + contentDescription = "Wifi", + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + }, + ) { + Text("Wifi") + } + } + item { + ListSubHeader( + modifier = + Modifier.minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Display") + } + } + item { + Button( + modifier = + Modifier.fillMaxWidth() + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text("Change Watchface") + } + } + item { + Button( + modifier = + Modifier.fillMaxWidth() + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = {}, + ) { + Text("Brightness") + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt b/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt new file mode 100644 index 0000000..340d53a --- /dev/null +++ b/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt @@ -0,0 +1,651 @@ +``` +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.annotation.Sampled +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.AmbientMode +import androidx.wear.compose.foundation.LocalAmbientModeManager +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.foundation.pager.HorizontalPager +import androidx.wear.compose.foundation.pager.VerticalPager +import androidx.wear.compose.foundation.pager.rememberPagerState +import androidx.wear.compose.foundation.rememberAmbientModeManager +import androidx.wear.compose.material3.AnimatedPage +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.EdgeButton +import androidx.wear.compose.material3.HorizontalPagerScaffold +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.VerticalPagerScaffold +import androidx.wear.compose.material3.onehandedgesture.LocalOneHandedGestureEnabled +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureDefaults +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureHorizontalPageIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePageIndicatorState +import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePriority +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicatorState +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureVerticalPageIndicator +import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture +import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun OneHandedGestureButtonSample() { + var label by remember { mutableStateOf("Gesturable Button") } + val onClick = { label = "Clicked/Gestured" } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + interactionSource = interactionSource, + onGestureLabel = "activate the button", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + onGesture = onClick, + ), + ) { + OneHandedGestureClickIndicator(gestureConfig, indicatorState) { + Text(label, modifier = Modifier.fillMaxWidth()) + } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureButtonInAmbientSample() { + var label by remember { mutableStateOf("Gesturable Button") } + val onClick = { label = "Clicked/Gestured" } + val interactionSource = remember { MutableInteractionSource() } + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + val activityAmbientModeManager = rememberAmbientModeManager() + var showGestureIndicator by remember { mutableStateOf(true) } + + CompositionLocalProvider(LocalAmbientModeManager provides activityAmbientModeManager) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + val isInAmbientMode = + LocalAmbientModeManager.current?.currentAmbientMode is AmbientMode.Ambient + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = + Modifier.fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + interactionSource = interactionSource, + enabledInAmbient = true, + onGestureLabel = "activate the button", + onGestureAvailable = { + // Bypass repeating the gesture indicator on every recomposition. + if (showGestureIndicator) { + coroutineScope.launch { indicatorState.showIndicator() } + showGestureIndicator = false + } + }, + onGesture = onClick, + ), + colors = + if (isInAmbientMode) ButtonDefaults.outlinedButtonColors() + else ButtonDefaults.buttonColors(), + border = + if (isInAmbientMode) ButtonDefaults.outlinedButtonBorder(enabled = true) + else null, + ) { + OneHandedGestureClickIndicator(gestureConfig, indicatorState) { + Text(label, modifier = Modifier.fillMaxWidth()) + } + } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureDisableButtonSample() { + var counter by remember { mutableIntStateOf(0) } + var enabled by remember { mutableStateOf(true) } + val interactionSource = remember { MutableInteractionSource() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SwitchButton(checked = enabled, onCheckedChange = { enabled = it }) { + Text("Gestures enabled") + } + Spacer(modifier = Modifier.height(6.dp)) + CompositionLocalProvider(LocalOneHandedGestureEnabled provides enabled) { + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + Button( + onClick = {}, + interactionSource = interactionSource, + modifier = + Modifier.oneHandedGesture( + gestureConfiguration = gestureConfig, + interactionSource = interactionSource, + onGestureLabel = "increase the counter", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + onGesture = { counter++ }, + ), + ) { + OneHandedGestureClickIndicator(gestureConfig, indicatorState) { + Text("Gestured $counter times") + } + } + } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureTransformingLazyColumnSample() { + val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val onClick = + remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } + val scrollState = rememberTransformingLazyColumnState() + val coroutineScope = rememberCoroutineScope() + + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Clickable, + ) + val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } + + val scrollGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable, + ) + val scrollIndicatorState = + remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } + + ScreenScaffold( + scrollState = scrollState, + edgeButton = { + EdgeButton( + onClick = onClick, + interactionSource = buttonInteractionSource, + modifier = + if (scrollState.canScrollForward) { + Modifier + } else { + // Apply the one-handed gesture modifier only when the container cannot + // scroll further, ensuring the EdgeButton is fully visible and interactive + Modifier.oneHandedGesture( + gestureConfiguration = buttonGestureConfig, + interactionSource = buttonInteractionSource, + onGestureLabel = "close", + onGestureAvailable = { + coroutineScope.launch { buttonIndicatorState.showIndicator() } + }, + onGesture = onClick, + ) + } then + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + overscrollEffect = rememberOverscrollEffect(), + ), + ) { + OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { + Text("Close") + } + } + }, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = scrollGestureConfig, + indicatorState = scrollIndicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + ) + }, + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = + Modifier.fillMaxSize() + .oneHandedGesture( + gestureConfiguration = scrollGestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { scrollIndicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }, + ), + ) { + items(10) { Text("Item $it") } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureScalingLazyColumnSample() { + val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val onClick = + remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } + val scrollState = rememberScalingLazyListState() + val coroutineScope = rememberCoroutineScope() + + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Clickable, + ) + val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } + + val scrollGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable, + ) + val scrollIndicatorState = + remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } + + ScreenScaffold( + scrollState = scrollState, + edgeButton = { + EdgeButton( + onClick = onClick, + interactionSource = buttonInteractionSource, + modifier = + if (scrollState.canScrollForward) { + Modifier + } else { + // Apply the one-handed gesture modifier only when the container cannot + // scroll further, ensuring the EdgeButton is fully visible and interactive + Modifier.oneHandedGesture( + gestureConfiguration = buttonGestureConfig, + interactionSource = buttonInteractionSource, + onGestureLabel = "close", + onGestureAvailable = { + coroutineScope.launch { buttonIndicatorState.showIndicator() } + }, + onGesture = onClick, + ) + } then + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + overscrollEffect = rememberOverscrollEffect(), + ), + ) { + OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { + Text("Close") + } + } + }, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = scrollGestureConfig, + indicatorState = scrollIndicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + ) + }, + ) { contentPadding -> + ScalingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = + Modifier.fillMaxSize() + .oneHandedGesture( + gestureConfiguration = scrollGestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { scrollIndicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }, + ), + autoCentering = null, + ) { + items(10) { Text("Item $it") } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureTransformingLazyColumnScrollToNextItemSample() { + val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val onClick = + remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } + val scrollState = rememberTransformingLazyColumnState() + val coroutineScope = rememberCoroutineScope() + + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Clickable, + ) + val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } + + val scrollGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable, + ) + val scrollIndicatorState = + remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } + + ScreenScaffold( + scrollState = scrollState, + edgeButton = { + EdgeButton( + onClick = onClick, + interactionSource = buttonInteractionSource, + modifier = + if (scrollState.canScrollForward) { + Modifier + } else { + // Apply the one-handed gesture modifier only when the container cannot + // scroll further, ensuring the EdgeButton is fully visible and interactive + Modifier.oneHandedGesture( + gestureConfiguration = buttonGestureConfig, + interactionSource = buttonInteractionSource, + onGestureLabel = "close", + onGestureAvailable = { + coroutineScope.launch { buttonIndicatorState.showIndicator() } + }, + onGesture = onClick, + ) + } then + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + overscrollEffect = rememberOverscrollEffect(), + ), + ) { + OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { + Text("Close") + } + } + }, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = scrollGestureConfig, + indicatorState = scrollIndicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + ) + }, + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = + Modifier.fillMaxSize() + .oneHandedGesture( + gestureConfiguration = scrollGestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { scrollIndicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) }, + ), + ) { + items(10) { Text("Item $it") } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureScalingLazyColumnScrollToNextItemSample() { + val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val onClick = + remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } + val scrollState = rememberScalingLazyListState() + val coroutineScope = rememberCoroutineScope() + + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Clickable, + ) + val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } + + val scrollGestureConfig = + rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable, + ) + val scrollIndicatorState = + remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } + + ScreenScaffold( + scrollState = scrollState, + edgeButton = { + EdgeButton( + onClick = onClick, + interactionSource = buttonInteractionSource, + modifier = + if (scrollState.canScrollForward) { + Modifier + } else { + // Apply the one-handed gesture modifier only when the container cannot + // scroll further, ensuring the EdgeButton is fully visible and interactive + Modifier.oneHandedGesture( + gestureConfiguration = buttonGestureConfig, + interactionSource = buttonInteractionSource, + onGestureLabel = "close", + onGestureAvailable = { + coroutineScope.launch { buttonIndicatorState.showIndicator() } + }, + onGesture = onClick, + ) + } then + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + overscrollEffect = rememberOverscrollEffect(), + ), + ) { + OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { + Text("Close") + } + } + }, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = scrollGestureConfig, + indicatorState = scrollIndicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + ) + }, + ) { contentPadding -> + ScalingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = + Modifier.fillMaxSize() + .oneHandedGesture( + gestureConfiguration = scrollGestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { scrollIndicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) }, + ), + autoCentering = null, + ) { + items(10) { Text("Item $it") } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureHorizontalPagerSample() { + val pagerState = rememberPagerState(pageCount = { 10 }) + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGesturePageIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + HorizontalPagerScaffold( + pagerState = pagerState, + pageIndicator = { + OneHandedGestureHorizontalPageIndicator( + gestureConfiguration = gestureConfig, + indicatorState = indicatorState, + pagerState = pagerState, + ) + }, + ) { + HorizontalPager( + state = pagerState, + modifier = + Modifier.oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "scroll to the next page", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + ) { + OneHandedGestureDefaults.scrollToNextPage(pagerState) + }, + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe left and right") + } + } + } + } + } +} + +@Sampled +@Composable +fun OneHandedGestureVerticalPagerSample() { + val pagerState = rememberPagerState(pageCount = { 10 }) + val gestureConfig = + rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) + val indicatorState = remember { OneHandedGesturePageIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + VerticalPagerScaffold( + pagerState = pagerState, + pageIndicator = { + OneHandedGestureVerticalPageIndicator( + gestureConfiguration = gestureConfig, + indicatorState = indicatorState, + pagerState = pagerState, + ) + }, + ) { + VerticalPager( + state = pagerState, + modifier = + Modifier.oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "scroll to the next page", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + ) { + OneHandedGestureDefaults.scrollToNextPage(pagerState) + }, + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe up and down") + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt new file mode 100644 index 0000000..dc164fc --- /dev/null +++ b/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt @@ -0,0 +1,57 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.OpenOnPhoneDialog +import androidx.wear.compose.material3.OpenOnPhoneDialogDefaults +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.openOnPhoneDialogCurvedText + +@Sampled +@Composable +fun OpenOnPhoneDialogSample() { + var showConfirmation by remember { mutableStateOf(false) } + + Box(Modifier.fillMaxSize()) { + FilledTonalButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showConfirmation = true }, + label = { Text("Open on phone") }, + ) + } + + val text = OpenOnPhoneDialogDefaults.text + val style = OpenOnPhoneDialogDefaults.curvedTextStyle + OpenOnPhoneDialog( + visible = showConfirmation, + onDismissRequest = { showConfirmation = false }, + curvedText = { openOnPhoneDialogCurvedText(text = text, style = style) }, + ) +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt new file mode 100644 index 0000000..6b5828e --- /dev/null +++ b/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt @@ -0,0 +1,110 @@ +``` +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.pager.HorizontalPager +import androidx.wear.compose.foundation.pager.VerticalPager +import androidx.wear.compose.foundation.pager.rememberPagerState +import androidx.wear.compose.material3.AnimatedPage +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.HorizontalPageIndicator +import androidx.wear.compose.material3.HorizontalPagerScaffold +import androidx.wear.compose.material3.PagerScaffoldDefaults +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.VerticalPageIndicator +import androidx.wear.compose.material3.VerticalPagerScaffold + +@Sampled +@Composable +fun HorizontalPageIndicatorWithPagerSample(navigateBack: () -> Unit) { + val pageCount = 9 + val pagerState = rememberPagerState { pageCount } + + Box { + HorizontalPagerScaffold( + pagerState = pagerState, + pageIndicator = { HorizontalPageIndicator(pagerState = pagerState) }, + ) { + HorizontalPager( + state = pagerState, + flingBehavior = + PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState), + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe left and right") + if (page == 0) { + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = navigateBack) { Text("Exit") } + } + } + } + } + } + } +} + +@Sampled +@Composable +fun VerticalPageIndicatorWithPagerSample() { + val pageCount = 9 + val pagerState = rememberPagerState { pageCount } + + Box { + VerticalPagerScaffold( + pagerState = pagerState, + pageIndicator = { VerticalPageIndicator(pagerState = pagerState) }, + ) { + VerticalPager( + state = pagerState, + flingBehavior = + PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState), + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe up and down") + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt new file mode 100644 index 0000000..1d2aced --- /dev/null +++ b/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt @@ -0,0 +1,204 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.pager.HorizontalPager +import androidx.wear.compose.foundation.pager.PagerDefaults +import androidx.wear.compose.foundation.pager.VerticalPager +import androidx.wear.compose.foundation.pager.rememberPagerState +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults +import androidx.wear.compose.material3.AnimatedPage +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.HorizontalPagerScaffold +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.PagerScaffoldDefaults +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.VerticalPagerScaffold + +@Sampled +@Composable +fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) { + AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + + HorizontalPagerScaffold(pagerState = pagerState) { + HorizontalPager( + state = pagerState, + flingBehavior = + PagerDefaults.snapFlingBehavior( + state = pagerState, + maxFlingPages = 1, + snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold, + snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + ), + rotaryScrollableBehavior = null, + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe left and right") + if (page == 0) { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = navigateBack) { Text("Exit") } + } + } + } + } + } + } + } +} + +@Sampled +@Composable +fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) { + AppScaffold { + val pagerState = rememberPagerState(pageCount = { 3 }) + + HorizontalPagerScaffold(pagerState = pagerState) { + HorizontalPager( + state = pagerState, + flingBehavior = + PagerDefaults.snapFlingBehavior( + state = pagerState, + maxFlingPages = 0, + snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, + snapAnimationSpec = PagerDefaults.SnapAnimationSpec, + ), + rotaryScrollableBehavior = + RotaryScrollableDefaults.snapBehavior( + pagerState = pagerState, + snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity, + ), + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe left and right") + if (page == 0) { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = navigateBack) { Text("Exit") } + } + } + } + } + } + } + } +} + +@Sampled +@Composable +fun VerticalPagerScaffoldSample() { + AppScaffold { + val pagerState = rememberPagerState(pageCount = { 10 }) + + VerticalPagerScaffold(pagerState = pagerState) { + VerticalPager( + state = pagerState, + flingBehavior = + PagerDefaults.snapFlingBehavior( + state = pagerState, + maxFlingPages = 1, + snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold, + snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + ), + rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(pagerState), + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe up and down") + } + } + } + } + } + } +} + +@Sampled +@Composable +fun VerticalPagerScaffoldWithLowSensitivitySample() { + AppScaffold { + val pagerState = rememberPagerState(pageCount = { 3 }) + + VerticalPagerScaffold(pagerState = pagerState) { + VerticalPager( + state = pagerState, + flingBehavior = + PagerDefaults.snapFlingBehavior( + state = pagerState, + maxFlingPages = 0, + snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, + snapAnimationSpec = PagerDefaults.SnapAnimationSpec, + ), + rotaryScrollableBehavior = + RotaryScrollableDefaults.snapBehavior( + pagerState = pagerState, + snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity, + ), + ) { page -> + AnimatedPage(pageIndex = page, pagerState = pagerState) { + ScreenScaffold { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Page #$page") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Swipe up and down") + } + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt new file mode 100644 index 0000000..530b02f --- /dev/null +++ b/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt @@ -0,0 +1,111 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.Picker +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.rememberPickerState +import kotlinx.coroutines.launch + +@Sampled +@Composable +fun SimplePicker() { + val items = listOf("One", "Two", "Three", "Four", "Five") + val state = rememberPickerState(items.size) + // We forward scroll gestures from the whole screen to the Picker which makes this sample + // accessible for 2-finger vertical scrolling. + Box( + modifier = + Modifier.fillMaxSize() + .scrollable( + state = state, + orientation = Orientation.Vertical, + reverseDirection = true, + ), + contentAlignment = Alignment.Center, + ) { + val selectedLabel by remember { + derivedStateOf { "Selected: ${items[state.selectedOptionIndex]}" } + } + Text( + modifier = Modifier.align(Alignment.TopCenter).padding(top = 10.dp), + text = selectedLabel, + ) + Picker( + modifier = Modifier.size(100.dp, 100.dp), + state = state, + contentDescription = { "${state.selectedOptionIndex + 1}" }, + ) { + Text(items[it]) + } + } +} + +@Sampled +@Composable +fun PickerScrollToOption() { + val coroutineScope = rememberCoroutineScope() + val state = rememberPickerState(initialNumberOfOptions = 10) + Picker( + state = state, + verticalSpacing = 4.dp, + contentDescription = { "${state.selectedOptionIndex + 1}" }, + ) { + Button( + onClick = { coroutineScope.launch { state.scrollToOption(it) } }, + label = { Text("$it") }, + ) + } +} + +@Sampled +@Composable +fun PickerAnimateScrollToOption() { + val coroutineScope = rememberCoroutineScope() + val state = rememberPickerState(initialNumberOfOptions = 10) + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Picker( + state = state, + verticalSpacing = 4.dp, + contentDescription = { "${state.selectedOptionIndex + 1}" }, + ) { + Button( + onClick = { coroutineScope.launch { state.animateScrollToOption(it) } }, + label = { Text("$it") }, + ) + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt new file mode 100644 index 0000000..943bca1 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt @@ -0,0 +1,182 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.placeholder +import androidx.wear.compose.material3.placeholderShimmer +import androidx.wear.compose.material3.rememberPlaceholderState +import kotlinx.coroutines.delay + +/** + * This sample applies placeholders directly over the content that is waiting to be loaded. This + * approach is suitable for situations where the developer has an approximate knowledge of how big + * the content is going to be and it doesn't have cached data that can be shown. + */ +@Sampled +@Composable +fun ButtonWithIconAndLabelAndPlaceholders() { + var labelText by remember { mutableStateOf("") } + var imageVector: ImageVector? by remember { mutableStateOf(null) } + val buttonPlaceholderState = + rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null) + + FilledTonalButton( + onClick = { /* Do something */ }, + enabled = true, + modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState), + label = { + Text( + text = labelText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth().placeholder(buttonPlaceholderState), + ) + }, + icon = { + Box( + modifier = + Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState) + ) { + if (imageVector != null) { + Icon( + imageVector = imageVector!!, + contentDescription = "Heart", + modifier = + Modifier.wrapContentSize(align = Alignment.Center) + .size(ButtonDefaults.IconSize) + .fillMaxSize(), + ) + } + } + }, + ) + // Simulate content loading completing in stages + LaunchedEffect(Unit) { + delay(2000) + imageVector = Icons.Filled.Favorite + delay(1000) + labelText = "A label" + } +} + +/** + * This sample doesn't use placeholders for the label as there is some cached data that can be shown + * while loading. + */ +@Sampled +@Composable +fun ButtonWithIconAndLabelCachedData() { + var labelText by remember { mutableStateOf("Cached Data") } + var imageVector: ImageVector? by remember { mutableStateOf(null) } + val buttonPlaceholderState = + rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null) + + // Put placeholderShimmer in the container and placeholder in the elements of the content that + // have no cached data. + FilledTonalButton( + onClick = { /* Do something */ }, + enabled = true, + modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState), + label = { + Text( + text = labelText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + }, + icon = { + Box( + modifier = + Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState) + ) { + if (imageVector != null) { + Icon( + imageVector = imageVector!!, + contentDescription = "Heart", + modifier = + Modifier.wrapContentSize(align = Alignment.Center) + .size(ButtonDefaults.IconSize) + .fillMaxSize(), + ) + } + } + }, + ) + // Simulate content loading completing in stages + LaunchedEffect(Unit) { + delay(2000) + imageVector = Icons.Filled.Favorite + delay(1000) + labelText = "A label" + } +} + +/** + * This sample applies a placeholder and placeholderShimmer directly over a single composable. + * + * Note that the modifier ordering is important, the placeholderShimmer must be before the + * placeholder in the modifier chain - otherwise the shimmer will be drawn underneath the + * placeholder and will not be visible. + */ +@Sampled +@Composable +fun TextPlaceholder() { + var labelText by remember { mutableStateOf("") } + val placeholderState = rememberPlaceholderState(isVisible = labelText.isEmpty()) + + Text( + text = labelText, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = + Modifier.width(90.dp).placeholderShimmer(placeholderState).placeholder(placeholderState), + ) + + // Simulate content loading + LaunchedEffect(Unit) { + delay(3000) + labelText = "A label" + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt new file mode 100644 index 0000000..97332f1 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt @@ -0,0 +1,263 @@ +``` +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.ArcProgressIndicator +import androidx.wear.compose.material3.ArcProgressIndicatorDefaults +import androidx.wear.compose.material3.CircularProgressIndicator +import androidx.wear.compose.material3.CircularProgressIndicatorDefaults +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.IconButton +import androidx.wear.compose.material3.IconButtonDefaults +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ProgressIndicatorDefaults +import androidx.wear.compose.material3.SegmentedCircularProgressIndicator +import androidx.wear.compose.material3.drawCircularProgressIndicator + +@Sampled +@Composable +fun FullScreenProgressIndicatorSample() { + Box( + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .padding(CircularProgressIndicatorDefaults.FullScreenPadding) + .fillMaxSize() + ) { + CircularProgressIndicator(progress = { 0.25f }, startAngle = 120f, endAngle = 60f) + } +} + +@Sampled +@Composable +fun MediaButtonProgressIndicatorSample() { + var isPlaying by remember { mutableStateOf(false) } + val buttonPadding = 4.dp + val progressStrokeWidth = 4.dp + val progress = 0.75f + + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { + // The CircularProgressIndicator should be around the IconButton, with an extra gap between + // then of 'buttonPadding'. We multiply by 2 because the size includes progressStrokeWidth + // at top and bottom and the buttonPadding at top and bottom. + CircularProgressIndicator( + modifier = + Modifier.align(Alignment.Center) + .size( + IconButtonDefaults.DefaultButtonSize + + progressStrokeWidth * 2 + + buttonPadding * 2 + ), + progress = { progress }, + strokeWidth = progressStrokeWidth, + ) + + IconButton( + modifier = + Modifier.align(Alignment.Center) + .semantics { + // Set custom progress semantics for accessibility. + contentDescription = + String.format( + "Play/pause button, track progress: %.0f%%", + progress * 100, + ) + } + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerLow), + onClick = { isPlaying = !isPlaying }, + ) { + Icon( + imageVector = if (isPlaying) Icons.Filled.Close else Icons.Filled.PlayArrow, + contentDescription = null, + ) + } + } +} + +@Sampled +@Composable +fun OverflowProgressIndicatorSample() { + Box( + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .padding(CircularProgressIndicatorDefaults.FullScreenPadding) + .fillMaxSize() + ) { + CircularProgressIndicator( + // Overflow value of 120% + progress = { 1.2f }, + allowProgressOverflow = true, + startAngle = 120f, + endAngle = 60f, + ) + } +} + +@Sampled +@Composable +fun SmallValuesProgressIndicatorSample() { + Box { + CircularProgressIndicator( + // Small progress values like 2% will be rounded up to at least the stroke width. + progress = { 0.02f }, + modifier = + Modifier.fillMaxSize().padding(CircularProgressIndicatorDefaults.FullScreenPadding), + startAngle = 120f, + endAngle = 60f, + strokeWidth = 10.dp, + colors = + ProgressIndicatorDefaults.colors( + indicatorColor = Color.Green, + trackColor = Color.White, + ), + ) + } +} + +@Sampled +@Composable +fun CircularProgressIndicatorCustomAnimationSample() { + val animatedProgress = remember { Animatable(0f) } + val colors = + ProgressIndicatorDefaults.colors(indicatorColor = Color.Green, trackColor = Color.White) + + LaunchedEffect(Unit) { + animatedProgress.animateTo(1f, tween(durationMillis = 1024, easing = LinearEasing)) + animatedProgress.animateTo(0f, tween(durationMillis = 1024, easing = LinearEasing)) + } + + Box( + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .padding(CircularProgressIndicatorDefaults.FullScreenPadding) + .fillMaxSize() + ) { + // Draw the circular progress indicator with custom animation + Spacer( + Modifier.fillMaxSize().focusable().drawBehind { + drawCircularProgressIndicator( + progress = animatedProgress.value, + strokeWidth = 10.dp, + colors = colors, + startAngle = 120f, + endAngle = 60f, + ) + } + ) + } +} + +@Sampled +@Composable +fun IndeterminateProgressIndicatorSample() { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } +} + +@Sampled +@Composable +fun IndeterminateProgressArcSample() { + Box(modifier = Modifier.fillMaxSize()) { + ArcProgressIndicator( + modifier = + Modifier.align(Alignment.Center) + .size(ArcProgressIndicatorDefaults.recommendedIndeterminateDiameter) + ) + } +} + +@Sampled +@Composable +fun SegmentedProgressIndicatorSample() { + Box( + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .padding(CircularProgressIndicatorDefaults.FullScreenPadding) + .fillMaxSize() + ) { + SegmentedCircularProgressIndicator(segmentCount = 5, progress = { 0.5f }) + } +} + +@Sampled +@Composable +fun SegmentedProgressIndicatorBinarySample() { + Box( + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .padding(CircularProgressIndicatorDefaults.FullScreenPadding) + .fillMaxSize() + ) { + SegmentedCircularProgressIndicator(segmentCount = 5, segmentValue = { it % 2 != 0 }) + } +} + +@Sampled +@Composable +fun SmallSegmentedProgressIndicatorSample() { + Box(modifier = Modifier.fillMaxSize()) { + SegmentedCircularProgressIndicator( + segmentCount = 6, + progress = { 0.75f }, + modifier = Modifier.align(Alignment.Center).size(80.dp), + ) + } +} + +@Sampled +@Composable +fun SmallSegmentedProgressIndicatorBinarySample() { + Box(modifier = Modifier.fillMaxSize()) { + SegmentedCircularProgressIndicator( + segmentCount = 8, + segmentValue = { it % 2 != 0 }, + modifier = Modifier.align(Alignment.Center).size(80.dp), + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt new file mode 100644 index 0000000..3c1b941 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt @@ -0,0 +1,109 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.RadioButton +import androidx.wear.compose.material3.SplitRadioButton +import androidx.wear.compose.material3.Text + +@Sampled +@Preview +@Composable +fun RadioButtonSample() { + Column(modifier = Modifier.selectableGroup().fillMaxSize()) { + var selectedButton by remember { mutableStateOf(0) } + // RadioButton uses the Radio selection control by default. + RadioButton( + label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + secondaryLabel = { + Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + selected = selectedButton == 0, + onSelect = { selectedButton = 0 }, + icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, + enabled = true, + ) + Spacer(modifier = Modifier.height(4.dp)) + RadioButton( + label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + secondaryLabel = { + Text("With secondary label", maxLines = 3, overflow = TextOverflow.Ellipsis) + }, + selected = selectedButton == 1, + onSelect = { selectedButton = 1 }, + icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, + enabled = true, + ) + } +} + +@Sampled +@Preview +@Composable +fun SplitRadioButtonSample() { + Column(modifier = Modifier.selectableGroup().padding(horizontal = 10.dp)) { + var selectedButton by remember { mutableStateOf(0) } + // SplitRadioButton uses the Radio selection control by default. + SplitRadioButton( + label = { Text("First Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + selected = selectedButton == 0, + onSelectionClick = { selectedButton = 0 }, + selectionContentDescription = "First", + onContainerClick = { + /* Do something */ + }, + containerClickLabel = "click", + modifier = Modifier.fillMaxWidth(), + enabled = true, + ) + Spacer(modifier = Modifier.height(4.dp)) + SplitRadioButton( + label = { Text("Second Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + selected = selectedButton == 1, + onSelectionClick = { selectedButton = 1 }, + selectionContentDescription = "Second", + onContainerClick = { + /* Do something */ + }, + containerClickLabel = "click", + modifier = Modifier.fillMaxWidth(), + enabled = true, + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt new file mode 100644 index 0000000..9620d69 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt @@ -0,0 +1,146 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.EdgeButton +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Preview +@Sampled +@Composable +fun ScaffoldSample() { + // Declare just one [AppScaffold] per app such as in the activity. + // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible + // during in-app transitions such as swipe-to-dismiss. + AppScaffold { + val transformationSpec = rememberTransformationSpec() + + // Define the navigation hierarchy within the AppScaffold, + // such as using SwipeDismissableNavHost. + // For this sample, we will define a single screen inline. + val listState = rememberTransformingLazyColumnState() + + // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator, + // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton. + ScreenScaffold(scrollState = listState) { contentPadding -> + TransformingLazyColumn( + state = listState, + contentPadding = contentPadding, + modifier = Modifier.fillMaxSize(), + ) { + items(10) { + Button( + onClick = {}, + label = { Text("Item ${it + 1}") }, + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .fillMaxWidth(), + ) + } + } + } + } +} + +@Preview +@Sampled +@Composable +fun ScaffoldWithTLCEdgeButtonSample() { + // Declare just one [AppScaffold] per app such as in the activity. + // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible + // during in-app transitions such as swipe-to-dismiss. + AppScaffold(modifier = Modifier.background(Color.Black)) { + val transformationSpec = rememberTransformationSpec() + + // Define the navigation hierarchy within the AppScaffold, + // such as using SwipeDismissableNavHost. + // For this sample, we will define a single screen inline. + val listState = rememberTransformingLazyColumnState() + // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator, + // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton. + ScreenScaffold( + scrollState = listState, + // Define custom spacing between [EdgeButton] and [TransformingLazyColumn]. + edgeButtonSpacing = 15.dp, + edgeButton = { + EdgeButton( + onClick = {}, + modifier = + // In case user starts scrolling from the EdgeButton. + Modifier.scrollable( + listState, + orientation = Orientation.Vertical, + reverseDirection = true, + // An overscroll effect should be applied to the EdgeButton for proper + // scrolling behavior. + overscrollEffect = rememberOverscrollEffect(), + ), + ) { + Text("Clear All") + } + }, + ) { contentPadding -> + TransformingLazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + // Bottom spacing is derived from [ScreenScaffold.edgeButtonSpacing]. + contentPadding = contentPadding, + ) { + items(4) { + Button( + onClick = {}, + label = { Text("Item ${it + 1}") }, + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .fillMaxWidth(), + ) + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt new file mode 100644 index 0000000..7205759 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt @@ -0,0 +1,97 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.ScrollInfoProvider +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults +import androidx.wear.compose.material3.ScreenStage +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TimeText +import androidx.wear.compose.material3.curvedText +import androidx.wear.compose.material3.scrollAway +import androidx.wear.compose.material3.timeTextSeparator + +@Sampled +@Composable +fun ScrollAwaySample() { + val state = rememberTransformingLazyColumnState() + + Box(modifier = Modifier.fillMaxSize()) { + TransformingLazyColumn(state = state, modifier = Modifier.fillMaxSize()) { + item { + ListHeader( + modifier = + Modifier.minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "TLC", + textAlign = TextAlign.Center, + ) + } + } + items(50) { + FilledTonalButton( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 36.dp) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ), + onClick = {}, + label = { Text("Item ${it + 1}") }, + ) + } + } + TimeText( + // In practice, it is recommended to use the [AppScaffold] and [ScreenScaffold], + // so that the Material3 scroll away behavior is provided by default, rather than using + // [Modifier.scrollAway] directly. + modifier = + Modifier.scrollAway( + scrollInfoProvider = ScrollInfoProvider(state), + screenStage = { + if (state.isScrollInProgress) ScreenStage.Scrolling else ScreenStage.Idle + }, + ), + content = { time -> + curvedText("List") + timeTextSeparator() + curvedText(time) + }, + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt new file mode 100644 index 0000000..95f8ab8 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt @@ -0,0 +1,66 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ScreenScaffoldDefaults +import androidx.wear.compose.material3.ScrollIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TimeText + +@Sampled +@Preview +@Composable +fun ScrollIndicatorWithTLCSample() { + val scrollState = rememberTransformingLazyColumnState() + Box(modifier = Modifier.fillMaxSize()) { + TransformingLazyColumn( + modifier = Modifier.background(Color.Black), + state = scrollState, + contentPadding = ScreenScaffoldDefaults.contentPadding, + ) { + items(15) { + Button( + onClick = {}, + label = { Text("Button $it") }, + modifier = + Modifier.minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .fillMaxWidth(), + ) + } + } + ScrollIndicator(modifier = Modifier.align(Alignment.CenterEnd), state = scrollState) + TimeText() + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt new file mode 100644 index 0000000..25bb251 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt @@ -0,0 +1,86 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.wear.compose.material3.Slider +import androidx.wear.compose.material3.SliderDefaults + +@Sampled +@Composable +fun SliderSample() { + var value by remember { mutableStateOf(4.5f) } + Slider( + value = value, + onValueChange = { value = it }, + valueRange = 3f..6f, + steps = 5, + segmented = false, + ) +} + +@Sampled +@Composable +fun ChangedSliderSample() { + val initialValue = 4.5f + var value by remember { mutableStateOf(5.0f) } + Slider( + value = value, + onValueChange = { value = it }, + valueRange = 3f..6f, + steps = 5, + segmented = false, + colors = + if (value == initialValue) { + SliderDefaults.sliderColors() + } else { + SliderDefaults.variantSliderColors() + }, + ) +} + +@Sampled +@Composable +fun SliderSegmentedSample() { + var value by remember { mutableStateOf(2f) } + Slider( + value = value, + onValueChange = { value = it }, + valueRange = 1f..4f, + steps = 2, + segmented = true, + ) +} + +@Sampled +@Composable +fun SliderWithIntegerSample() { + var value by remember { mutableStateOf(4) } + Slider( + value = value, + onValueChange = { value = it }, + valueProgression = 0..10, + segmented = false, + ) +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt b/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt new file mode 100644 index 0000000..ea7c164 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt @@ -0,0 +1,148 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.Stepper +import androidx.wear.compose.material3.StepperDefaults +import androidx.wear.compose.material3.StepperLevelIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.rangeSemantics +import androidx.wear.compose.material3.samples.icons.HeadphoneIcon +import androidx.wear.compose.material3.samples.icons.VolumeDownIcon +import androidx.wear.compose.material3.samples.icons.VolumeUpIcon + +@Sampled +@Composable +fun StepperSample() { + var value by remember { mutableFloatStateOf(2f) } + val valueRange = remember { 0f..4f } + Box(modifier = Modifier.fillMaxSize()) { + Stepper( + value = value, + onValueChange = { value = it }, + valueRange = valueRange, + steps = 7, + decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, + increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, + ) { + Text(String.format("Value: %.1f".format(value))) + } + StepperLevelIndicator( + value = { value }, + valueRange = valueRange, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Sampled +@Composable +fun StepperWithIntegerSample() { + var value by remember { mutableIntStateOf(3) } + val valueProgression = remember { 0..10 } + Box(modifier = Modifier.fillMaxSize()) { + Stepper( + value = value, + onValueChange = { value = it }, + valueProgression = valueProgression, + decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, + increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, + ) { + Text(String.format("Value: %d".format(value))) + } + StepperLevelIndicator( + value = { value }, + valueProgression = valueProgression, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Sampled +@Composable +fun StepperWithRangeSemanticsSample() { + var value by remember { mutableFloatStateOf(2f) } + val valueRange = remember { 0f..4f } + val onValueChange = { i: Float -> value = i } + val steps = 7 + Box(modifier = Modifier.fillMaxSize()) { + Stepper( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + modifier = Modifier.rangeSemantics(value, true, onValueChange, valueRange, steps), + steps = steps, + decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, + increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, + ) { + Text("Value: $value") + } + StepperLevelIndicator( + value = { value }, + valueRange = valueRange, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Sampled +@Composable +fun StepperWithButtonSample() { + var value by remember { mutableFloatStateOf(2f) } + val valueRange = remember { 0f..4f } + Box(modifier = Modifier.fillMaxSize()) { + Stepper( + value = value, + onValueChange = { value = it }, + valueRange = valueRange, + increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, + decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, + steps = 7, + ) { + Text(String.format("Value: %.1f".format(value))) + Button( + onClick = {}, + modifier = Modifier.width(150.dp), + label = { Text(text = "This watch", modifier = Modifier.fillMaxWidth()) }, + secondaryLabel = { Text(text = "Headphones", modifier = Modifier.fillMaxWidth()) }, + icon = { HeadphoneIcon(24.dp) }, + ) + } + StepperLevelIndicator( + value = { value }, + valueRange = valueRange, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt new file mode 100644 index 0000000..6b3b7fa --- /dev/null +++ b/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt @@ -0,0 +1,165 @@ +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.CardDefaults +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TitleCard +import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec +import androidx.wear.compose.material3.lazy.TransformationVariableSpec +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Sampled +@Preview +@Composable +fun SurfaceTransformationOnCustomComponent() { + @Composable + fun MyCardComponent( + title: String, + body: String, + transformation: SurfaceTransformation, + modifier: Modifier = Modifier, + ) { + Column( + modifier = + modifier + .fillMaxWidth() + .paint( + transformation.createContainerPainter( + ColorPainter(color = Color.Gray), + shape = RoundedCornerShape(16.dp), + ) + ) + .graphicsLayer { with(transformation) { applyContainerTransformation() } } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text(title) + Text(body) + } + } + + val transformationSpec = rememberTransformationSpec() + + TransformingLazyColumn { + items(count = 100) { + MyCardComponent( + "Message #$it", + "This is a body", + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ), + ) + } + } +} + +@Sampled +@Preview +@Composable +fun SurfaceTransformationButtonSample() { + val transformationSpec = + rememberTransformationSpec( + ResponsiveTransformationSpec.smallScreen( + contentAlpha = + TransformationVariableSpec( + 0f, + transformationZoneEnterFraction = 0.4f, + transformationZoneExitFraction = 0.8f, + ), + containerAlpha = TransformationVariableSpec(0.3f), + ) + ) + + TransformingLazyColumn { + items(count = 100) { + Button( + onClick = {}, + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ), + ) { + Text("Button #$it") + } + } + } +} + +@Sampled +@Preview +@Composable +fun SurfaceTransformationCardSample() { + val transformationSpec = rememberTransformationSpec() + var expandedIndex by remember { mutableIntStateOf(-1) } + val state = rememberTransformingLazyColumnState() + + AppScaffold { + ScreenScaffold(state) { contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding) { + items(count = 100) { + TitleCard( + onClick = { expandedIndex = if (expandedIndex == it) -1 else it }, + title = { Text("Card #$it") }, + subtitle = { Text("Subtitle #$it") }, + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ), + ) { + if (it == expandedIndex) { + Text("Expanded content #$it") + } + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt new file mode 100644 index 0000000..75c908d --- /dev/null +++ b/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt @@ -0,0 +1,171 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.SwipeToDismissValue +import androidx.wear.compose.foundation.edgeSwipeToDismiss +import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState +import androidx.wear.compose.material3.CheckboxButton +import androidx.wear.compose.material3.FilledTonalButton +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.SwipeToDismissBox +import androidx.wear.compose.material3.Text + +@Sampled +@Composable +fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) { + SwipeToDismissBox(onDismissed = navigateBack) { isBackground -> + if (isBackground) { + Box( + modifier = + Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer) + ) + } else { + Column( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary) + } + } + } +} + +@Sampled +@Composable +fun StatefulSwipeToDismissBox() { + // State for managing a 2-level navigation hierarchy between + // MainScreen and ItemScreen composables. + // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation. + var showMainScreen by remember { mutableStateOf(true) } + val saveableStateHolder = rememberSaveableStateHolder() + + // Swipe gesture dismisses ItemScreen to return to MainScreen. + val state = rememberSwipeToDismissBoxState() + LaunchedEffect(state.currentValue) { + if (state.currentValue == SwipeToDismissValue.Dismissed) { + state.snapTo(SwipeToDismissValue.Default) + showMainScreen = !showMainScreen + } + } + + // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind + // the ItemScreen, otherwise there's no background to show. + SwipeToDismissBox( + state = state, + userSwipeEnabled = !showMainScreen, + backgroundKey = if (!showMainScreen) "MainKey" else "Background", + contentKey = if (showMainScreen) "MainKey" else "ItemKey", + ) { isBackground -> + if (isBackground || showMainScreen) { + // Best practice would be to use State Hoisting and leave this composable stateless. + // Here, we want to support MainScreen being shown from different destinations + // (either in the foreground or in the background during swiping) - that can be achieved + // using SaveableStateHolder and rememberSaveable as shown below. + saveableStateHolder.SaveableStateProvider( + key = "MainKey", + content = { + // Composable that maintains its own state + // and can be shown in foreground or background. + val checked = rememberSaveable { mutableStateOf(true) } + Column( + modifier = + Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + ) { + FilledTonalButton( + onClick = { showMainScreen = false }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Item details") + } + CheckboxButton( + label = { Text("Checkbox", maxLines = 1) }, + checked = checked.value, + onCheckedChange = { checked.value = it }, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + ) + } else { + Column( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("Show details here...", color = MaterialTheme.colorScheme.onPrimary) + Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary) + } + } + } +} + +@Sampled +@Composable +fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) { + val state = rememberSwipeToDismissBoxState() + + // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the + // modifier applies exists within a SwipeToDismissBox which shares the same state. + SwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> + val horizontalScrollState = rememberScrollState(0) + if (isBackground) { + Box( + modifier = + Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer) + ) + } else { + Box(modifier = Modifier.fillMaxSize()) { + Text( + modifier = + Modifier.align(Alignment.Center) + .edgeSwipeToDismiss(state) + .horizontalScroll(horizontalScrollState), + text = + "This text can be scrolled horizontally - to dismiss, swipe " + + "right from the left edge of the screen (called Edge Swiping)", + ) + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt new file mode 100644 index 0000000..9e87918 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt @@ -0,0 +1,68 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.SplitSwitchButton +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text + +@Sampled +@Preview +@Composable +fun SwitchButtonSample() { + var checked by remember { mutableStateOf(true) } + SwitchButton( + label = { Text("Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + secondaryLabel = { + Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + checked = checked, + onCheckedChange = { checked = it }, + icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, + enabled = true, + ) +} + +@Sampled +@Preview +@Composable +fun SplitSwitchButtonSample() { + var checked by remember { mutableStateOf(true) } + SplitSwitchButton( + label = { Text("Split Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, + checked = checked, + onCheckedChange = { checked = it }, + toggleContentDescription = "Split Switch Button Sample", + onContainerClick = { + /* Do something */ + }, + enabled = true, + ) +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt new file mode 100644 index 0000000..cf507f2 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt @@ -0,0 +1,115 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TextButton +import androidx.wear.compose.material3.TextButtonDefaults + +@Composable +@Sampled +fun TextButtonSample() { + TextButton(onClick = { /* Do something */ }) { Text(text = "ABC") } +} + +@Composable +@Sampled +fun FilledTextButtonSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.filledTextButtonColors(), + ) { + Text(text = "ABC") + } +} + +@Composable +@Sampled +fun FilledVariantTextButtonSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.filledVariantTextButtonColors(), + ) { + Text(text = "ABC") + } +} + +@Composable +@Sampled +fun LargeFilledTonalTextButtonSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.filledTonalTextButtonColors(), + modifier = Modifier.size(TextButtonDefaults.LargeButtonSize), + ) { + Text(text = "ABC", style = TextButtonDefaults.largeButtonTextStyle) + } +} + +@Composable +@Sampled +fun FilledTonalTextButtonSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.filledTonalTextButtonColors(), + ) { + Text(text = "ABC") + } +} + +@Composable +@Sampled +fun OutlinedTextButtonSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.outlinedTextButtonColors(), + border = ButtonDefaults.outlinedButtonBorder(enabled = true), + ) { + Text(text = "ABC") + } +} + +@Sampled +@Composable +fun TextButtonWithOnLongClickSample(onLongClick: () -> Unit) { + TextButton( + onClick = { /* Do something for onClick*/ }, + onLongClick = onLongClick, + onLongClickLabel = "Long click", + ) { + Text(text = "ABC") + } +} + +@Composable +@Sampled +fun TextButtonWithCornerAnimationSample() { + TextButton( + onClick = { /* Do something */ }, + colors = TextButtonDefaults.filledTextButtonColors(), + shapes = TextButtonDefaults.animatedShapes(), + ) { + Text(text = "ABC") + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt new file mode 100644 index 0000000..11f43da --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt @@ -0,0 +1,71 @@ +``` +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TextButtonDefaults +import androidx.wear.compose.material3.TextToggleButton +import androidx.wear.compose.material3.TextToggleButtonDefaults +import androidx.wear.compose.material3.touchTargetAwareSize + +@Sampled +@Composable +fun TextToggleButtonSample() { + var checked by remember { mutableStateOf(true) } + TextToggleButton( + checked = checked, + onCheckedChange = { checked = !checked }, + shapes = TextToggleButtonDefaults.animatedShapes(), + ) { + Text(text = if (checked) "On" else "Off") + } +} + +@Sampled +@Composable +fun TextToggleButtonVariantSample() { + var checked by remember { mutableStateOf(true) } + TextToggleButton( + checked = checked, + onCheckedChange = { checked = !checked }, + shapes = TextToggleButtonDefaults.variantAnimatedShapes(), + ) { + Text(text = if (checked) "On" else "Off") + } +} + +@Sampled +@Composable +fun LargeTextToggleButtonSample() { + var checked by remember { mutableStateOf(true) } + TextToggleButton( + checked = checked, + onCheckedChange = { checked = !checked }, + modifier = Modifier.touchTargetAwareSize(TextButtonDefaults.LargeButtonSize), + ) { + Text(text = if (checked) "On" else "Off", style = TextToggleButtonDefaults.largeTextStyle) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt new file mode 100644 index 0000000..fc15a6c --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt @@ -0,0 +1,65 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.style.TextOverflow +import androidx.wear.compose.foundation.CurvedModifier +import androidx.wear.compose.foundation.weight +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.TimeText +import androidx.wear.compose.material3.TimeTextDefaults +import androidx.wear.compose.material3.curvedText +import androidx.wear.compose.material3.timeTextCurvedText +import androidx.wear.compose.material3.timeTextSeparator + +@Sampled +@Composable +fun TimeTextClockOnly() { + // TimeText displays the current time by default. + TimeText() +} + +@Sampled +@Composable +fun TimeTextWithStatus() { + val style = TimeTextDefaults.timeTextStyle() + val primaryStyle = TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primary) + TimeText { time -> + timeTextCurvedText("ETA 12:48", style = primaryStyle) + timeTextSeparator(style) + timeTextCurvedText(time) + } +} + +@Sampled +@Composable +fun TimeTextWithStatusEllipsized() { + val style = TimeTextDefaults.timeTextStyle() + TimeText { time -> + curvedText( + "Long status that should be ellipsized.", + CurvedModifier.weight(1f), + overflow = TextOverflow.Ellipsis, + ) + timeTextSeparator(style) + timeTextCurvedText(time) + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt new file mode 100644 index 0000000..84704cb --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt @@ -0,0 +1,246 @@ +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy.Companion.ModulateAlpha +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumnItemScrollProgress +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ButtonGroup +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec +import androidx.wear.compose.material3.lazy.TransformationSpec +import androidx.wear.compose.material3.lazy.TransformationVariableSpec +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Composable +@Sampled +@Preview +fun CustomTransformationSpecSample() { + val transformationSpec = rememberTransformationSpec() + val morphingTransformationSpec = + object : TransformationSpec by transformationSpec { + override fun GraphicsLayerScope.applyContainerTransformation( + scrollProgress: TransformingLazyColumnItemScrollProgress + ) { + with(transformationSpec) { applyContainerTransformation(scrollProgress) } + rotationX = (scrollProgress.topOffsetFraction - 0.5f).coerceIn(0f..1f) * 270f + } + } + + TransformingLazyColumn( + contentPadding = PaddingValues(20.dp), + modifier = Modifier.background(Color.Black), + ) { + items(count = 100) { index -> + Button( + onClick = {}, + modifier = + Modifier.fillMaxWidth() + .transformedHeight(this, morphingTransformationSpec) + .graphicsLayer { + with(morphingTransformationSpec) { + applyContainerTransformation(scrollProgress) + } + }, + ) { + Text( + "Item $index", + modifier = + Modifier.graphicsLayer { + with(morphingTransformationSpec) { + applyContentTransformation(scrollProgress) + } + }, + ) + } + } + } +} + +@Composable +@Sampled +@Preview +fun CustomCompositingStrategyTransformationSpecSample() { + val transformationSpec = rememberTransformationSpec() + + TransformingLazyColumn( + contentPadding = PaddingValues(20.dp), + modifier = Modifier.background(Color.Black), + ) { + items(count = 100) { index -> + Button( + onClick = {}, + modifier = + Modifier.fillMaxWidth() + .transformedHeight(this, transformationSpec) + .graphicsLayer { + with(transformationSpec) { + applyContainerTransformation(scrollProgress) + } + // Using CompositingStrategy.ModulateAlpha can provide better + // performance when a container transformation involves alpha rendering, + // as it avoids an extra offscreen buffer. + // + // However, care must be taken with overlapping or transparent content + // inside the container. If the content itself uses alpha, ModulateAlpha + // can lead to multiple, incorrect alpha blending with the background + // (double-blending artifacts). + // + // To prevent this, the content's drawing layer must explicitly use + // CompositingStrategy.Offscreen to ensure internal elements are + // correctly pre-blended before the outer ModulateAlpha is applied. + compositingStrategy = ModulateAlpha + }, + ) { + Text( + text = "Item $index", + modifier = + Modifier.graphicsLayer { + // Ensure content layer uses CompositingStrategy.Offscreen when + // container uses CompositingStrategy.ModulateAlpha. + // This composition is required to guarantee correct visual blending of + // content that contains internal alpha or complex blending. + // compositingStrategy is set to CompositingStrategy.Offscreen inside + // applyContentTransformation. + with(transformationSpec) { applyContentTransformation(scrollProgress) } + }, + ) + } + } + } +} + +@Composable +@Sampled +@Preview +fun ResponsiveTransformationSpecButtonSample() { + val transformationSpec = + rememberTransformationSpec( + ResponsiveTransformationSpec.smallScreen( + // Makes the content disappear on the edges. + contentAlpha = TransformationVariableSpec(0f) + ), + ResponsiveTransformationSpec.largeScreen( + // Makes the content disappear on the edges, but a bit more aggressively. + contentAlpha = + TransformationVariableSpec(0f, transformationZoneEnterFraction = 0.2f) + ), + ) + TransformingLazyColumn( + contentPadding = PaddingValues(20.dp), + modifier = Modifier.background(Color.Black), + ) { + items(count = 100) { index -> + Button( + onClick = {}, + modifier = + Modifier.fillMaxWidth().transformedHeight(this@items, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Item $index") + } + } + } +} + +@Composable +@Sampled +@Preview +fun TransformationSpecButtonRowSample() { + // Use the spec derived from default small and large screen specs. + val transformationSpec = rememberTransformationSpec() + + TransformingLazyColumn( + contentPadding = PaddingValues(), + modifier = Modifier.background(Color.Black), + ) { + items(count = 100) { + val interactionSource1 = remember { MutableInteractionSource() } + val interactionSource2 = remember { MutableInteractionSource() } + + ButtonGroup( + modifier = + Modifier.fillMaxWidth() + .graphicsLayer { + with(transformationSpec) { + applyContainerTransformation(scrollProgress) + } + } + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + ) { + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource1), + interactionSource = interactionSource1, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + "L", + modifier = + Modifier.graphicsLayer { + with(transformationSpec) { + applyContentTransformation(scrollProgress) + } + }, + ) + } + } + Button( + onClick = {}, + modifier = Modifier.animateWidth(interactionSource2), + interactionSource = interactionSource2, + ) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + "R", + modifier = + Modifier.graphicsLayer { + with(transformationSpec) { + applyContentTransformation(scrollProgress) + } + }, + ) + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt new file mode 100644 index 0000000..d9d2980 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt @@ -0,0 +1,89 @@ +``` +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TitleCard +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnNotificationsSample() { + val state = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + data class NotificationItem(val title: String, val body: String) + + val notifications = + listOf( + NotificationItem( + "☕ Coffee Break?", + "Step away from the screen and grab a pick-me-up. Step away from the screen and grab a pick-me-up.", + ), + NotificationItem("🌟 You're Awesome!", "Just a little reminder in case you forgot 😊"), + NotificationItem("👀 Did you know?", "Check out [app name]'s latest feature update."), + NotificationItem("📅 Appointment Time", "Your meeting with [name] is in 15 minutes."), + ) + AppScaffold { + ScreenScaffold(state) { contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding) { + item { + ListHeader( + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec).animateItem(), + ) { + Text("Notifications") + } + } + items(notifications) { notification -> + TitleCard( + onClick = {}, + title = { + Text( + notification.title, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + ) + }, + subtitle = { Text(notification.body) }, + transformation = SurfaceTransformation(transformationSpec), + modifier = + Modifier.transformedHeight(this, transformationSpec).animateItem(), + ) + } + } + } + } +} +``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt new file mode 100644 index 0000000..379c818 --- /dev/null +++ b/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt @@ -0,0 +1,309 @@ +``` +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.material3.samples + +import androidx.annotation.Sampled +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider +import androidx.wear.compose.foundation.lazy.itemsIndexed +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.Card +import androidx.wear.compose.material3.CardDefaults +import androidx.wear.compose.material3.CompactButton +import androidx.wear.compose.material3.CompactButtonDefaults +import androidx.wear.compose.material3.EdgeButton +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults +import androidx.wear.compose.material3.OutlinedCard +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.firstVisibleItemLayoutItemInfo +import androidx.wear.compose.material3.lazy.layoutItemInfoOf +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.rememberTransformingLazyColumnFirstLayoutItemProvider +import androidx.wear.compose.material3.lazy.transformedHeight +import kotlinx.coroutines.delay + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnButtonsSample() { + val state = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + AppScaffold { + ScreenScaffold(state) { contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding) { + items(count = 50) { + Button( + onClick = {}, + modifier = + Modifier.minimumVerticalContentPadding( + ButtonDefaults.minimumVerticalListContentPadding + ) + .fillMaxWidth() + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Item $it") + } + } + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnMinimumVerticalContentPaddingSample() { + val transformationSpec = rememberTransformationSpec() + val state = rememberTransformingLazyColumnState() + var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) } + var nextElement by remember { mutableIntStateOf(10) } + + fun addElementAfter(index: Int) { + elements = + elements.subList(0, index + 1) + + listOf(nextElement++) + + elements.subList(index + 1, elements.count()) + } + + fun removeElementAt(index: Int) { + elements = elements.subList(0, index) + elements.subList(index + 1, elements.count()) + } + + AppScaffold { + ScreenScaffold( + state, + edgeButton = { + EdgeButton(onClick = { elements = elements.shuffled() }) { Text("Shuffle") } + }, + ) { contentPadding -> + TransformingLazyColumn(state = state, contentPadding = contentPadding) { + itemsIndexed(elements, key = { _, key -> key }) { index, key -> + if (key % 3 == 0) { + CompactButton( + onClick = { removeElementAt(index) }, + modifier = + Modifier.minimumVerticalContentPadding( + CompactButtonDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec) + .animateItem(), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("CompactButton $key") + } + } else if (key % 4 == 0) { + ListHeader( + modifier = + Modifier.minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("ListHeader heading") + } + } else { + Card( + onClick = {}, + modifier = + Modifier.fillMaxWidth() + .minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec) + .animateItem(), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Card $key") + Row { + Spacer(modifier = Modifier.weight(1f)) + CompactButton( + onClick = { removeElementAt(index) }, + enabled = elements.count() > 1, + ) { + Text("-") + } + CompactButton(onClick = { addElementAfter(index) }) { Text("+") } + } + } + } + } + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnFirstLayoutItemProviderSample() { + val state = rememberTransformingLazyColumnState() + val transformationSpec = rememberTransformationSpec() + + // This sample demonstrates how to use rememberTransformingLazyColumnFirstLayoutItemProvider + // to control the expansion direction of a dynamically resizing item. By using the voice input + // card's Bottom/End edge as the layout reference, the card predictably expands upwards. + // The constant key safely falls back to default layout behavior when scrolled off-screen. + val firstLayoutItemProvider = rememberTransformingLazyColumnFirstLayoutItemProvider { + state.layoutItemInfoOf( + itemKey = "voice_input", + itemEdge = TransformingLazyColumnFirstLayoutItemProvider.ItemEdge.End, + ) + } + + var isListening by remember { mutableStateOf(false) } + var lineCount by remember { mutableIntStateOf(0) } + // Simple loop simulating text lines arriving over time + LaunchedEffect(isListening) { + if (isListening) { + while (lineCount < 5) { + delay(300) + lineCount++ + } + isListening = false + } + } + + AppScaffold { + ScreenScaffold(state) { contentPadding -> + TransformingLazyColumn( + state = state, + contentPadding = contentPadding, + firstLayoutItemProvider = firstLayoutItemProvider, + ) { + items(count = 3, key = { "message_$it" }) { index -> + Card( + onClick = {}, + modifier = + Modifier.minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec) + .animateItem(placementSpec = null), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Previous message $index") + } + } + + item(key = "voice_input") { + val voiceText = (1..lineCount).joinToString("\n") { "Voice Input line $it" } + + OutlinedCard( + onClick = { + if (lineCount >= 5) lineCount = 0 + isListening = !isListening + }, + modifier = + Modifier.minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec) + .animateItem(placementSpec = null), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text(text = voiceText.ifEmpty { "Tap to speak" }) + } + } + } + } + } +} + +@Sampled +@Preview +@Composable +fun TransformingLazyColumnFirstVisibleItemLayoutItemProviderSample() { + val transformationSpec = rememberTransformationSpec() + val state = rememberTransformingLazyColumnState() + var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) } + var nextElement by remember { mutableIntStateOf(10) } + + fun addCardAfter(index: Int) { + elements = + elements.subList(0, index + 1) + + listOf(nextElement++) + + elements.subList(index + 1, elements.count()) + } + + fun removeCardAt(index: Int) { + elements = elements.subList(0, index) + elements.subList(index + 1, elements.count()) + } + + // This sample demonstrates how to use rememberTransformingLazyColumnFirstVisibleItemProvider + // to maintain a stable viewport top when items are added or removed dynamically. + // It achieves this by using the first visible item as the layout reference. + AppScaffold { + ScreenScaffold(state) { contentPadding -> + TransformingLazyColumn( + state = state, + contentPadding = contentPadding, + firstLayoutItemProvider = + rememberTransformingLazyColumnFirstLayoutItemProvider { + if (state.isScrollInProgress) null else state.firstVisibleItemLayoutItemInfo + }, + ) { + itemsIndexed(elements, key = { _, key -> key }) { index, cardKey -> + Card( + onClick = {}, + modifier = + Modifier.minimumVerticalContentPadding( + CardDefaults.minimumVerticalListContentPadding + ) + .transformedHeight(this, transformationSpec) + .animateItem(), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text("Card $cardKey") + Row { + Spacer(modifier = Modifier.weight(1f)) + CompactButton( + onClick = { removeCardAt(index) }, + enabled = elements.count() > 1, + ) { + Text("-") + } + CompactButton(onClick = { addCardAfter(index) }) { Text("+") } + } + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index 1748d43..c3ce954 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-22' + last-updated: '2026-07-30' keywords: - Jetpack Compose Glimmer - audio glasses @@ -250,7 +250,7 @@ feedback for interactive elements. If you are creating a Glimmer Icon component, read the: - **Developer Guidance:** [Jetpack Compose Glimmer: Icons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md) -- **API Source Code (Icon):** Use [references/icon-source.md](references/icon-source.md). +- **API Source Code (Icon):** Use [references/icon-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source). - **API Source Code (IconButton):** Use [references/iconbutton-source.md](references/iconbutton-source.md). - **API Source Code (IconSizes):** Use [references/iconsizes-source.md](references/iconsizes-source.md). @@ -264,7 +264,7 @@ different types. ##### Guidelines and usage -- **ListItem Slots:** Use the `ListItem` composable for rows. It provides predefined slots. Use [references/listitem-source.md](references/listitem-source.md) for reference. +- **ListItem Slots:** Use the `ListItem` composable for rows. It provides predefined slots. Use [references/listitem-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source) for reference. - **Visual Consistency:** When building lists of similar items, always use a consistent background color (typically `GlimmerTheme.colors.surface`) and corner radius (standard 36.dp) for every item. Don't vary these unless you are visually grouping different *types* of content. - **Integrated Title Chips:** Glimmer Lists support integrated title chips. **IF** you need a section header within a list: **THEN** enable the integrated title chip rather than adding a standalone `TitleChip` to maintain spatial consistency. - **Vertical Arrangement:** ALWAYS use `verticalArrangement = @@ -276,7 +276,7 @@ different types. If you are creating a Glimmer List component, read the: - **API Source Code (List):** Use [references/list-source.md](references/list-source.md). -- **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). +- **API Source Code (ListItem):** Use [references/listitem-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source). - **API Source Code (GlimmerLazyListState):** Use [references/liststate-source.md](references/liststate-source.md). #### Stacks diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md index 162b00c..3dcc770 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md @@ -1,6 +1,3 @@ -
- - Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \& Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) @@ -29,11 +26,31 @@ functionality.
-## Register the permissions launcher +## Before requesting glasses permissions -To request permissions for audio glasses and display glasses, first you use the -[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) -method to register the permissions launcher. +Review the [permission principles and workflows](https://developer.android.com/training/permissions/requesting#principles) to make sure you provide the +best experience for your users, like [checking whether the user has already +granted the runtime permissions](https://developer.android.com/training/permissions/requesting#already-granted) your app requires and [whether your app +should show a rationale](https://developer.android.com/training/permissions/requesting#explain) to the user before requesting glasses-specific +permissions. + +## Permission scenarios + +There are different scenarios you might encounter when requesting [dangerous +runtime permissions](https://developer.android.com/guide/topics/permissions/requesting#normal-dangerous) on audio and display glasses: + +- Request runtime permissions from a projected activity +- Request runtime permissions from a phone activity + +See the following sections in this guide for details about each scenario. + +### Request and handle runtime permissions from a projected activity + +To request permissions for audio glasses and display glasses, first use the +[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the +[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method to register the permissions +launcher. When the user has acted on the permission request, the callback +receives a map of permission names to their granted status. ```kotlin @@ -52,15 +69,18 @@ private val requestPermissionLauncher: ActivityResultLauncher -### Key points about the code +#### Key points about the code -- The code creates an [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method. The callback receives a map of permission names to their granted status. - You need to specify which permissions your app requires, such as [`Manifest.permission.CAMERA`](https://developer.android.com/reference/kotlin/android/Manifest.permission#camera) or [`Manifest.permission.RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio). +- Your app should handle both granted and denied results gracefully in the launcher's callback. -## Create the request function - -Next, you'll create a function that uses your app's permissions launcher to -request the permissions from the user at runtime. +To trigger the permission request flow, pass a list of +[`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) to your registered permission +launcher's `launch` method. The `ProjectedPermissionsRequestParams` object +bundles the requested manifest permissions together with a custom `rationale` +string. The `rationale` string must clearly and concisely explain why the app +requires access to the glasses' hardware features (such as the camera or +microphone). ```kotlin @@ -75,72 +95,17 @@ private fun requestHardwarePermissions() {
-### Key points about the code +#### Key points about the code -- The `requestHardwarePermissions` function builds a [`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) object. This object bundles the list of permissions your app needs and the user-facing rationale. Make the rationale clear and concise to explain why your app needs these permissions. - Calling `launch` on the launcher triggers the [permission request user flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow). -- Your app should handle both granted and denied results gracefully in the launcher's callback. -## Create the permissions check function - -Next, you'll create a function that can check whether the user has granted -permissions to your app. - - -```kotlin -private fun hasCameraPermission(): Boolean { - return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == - PackageManager.PERMISSION_GRANTED -} -``` - -
- -## Add the permission request logic - -And lastly, create the logic that uses these functions to check for and request -the permissions at runtime. - - -```kotlin -if (hasCameraPermission()) { - initializeGlassesFeatures() -} else { - requestHardwarePermissions() -} -``` - -
- -### Key points about the code - -- If the user has already granted your app the required permissions, the `initializeGlassesFeatures` function is called to initialize your app's experience. This function is defined as [part of your app's activity for AI - glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/first-activity#create-activity). - -## Understand the permission request user flow +#### Understand the permission request user flow When you launch a permission request using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method, the system initiates a coordinated user flow across both the glasses and the phone. -
- -> [!IMPORTANT] -> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on the glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow. -> ->
-> -> If your app already has an `Activity` displayed on the phone, you should use -> [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1), where -> the `deviceId` comes from calling the [`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) method on the -> [`Context`](https://developer.android.com/reference/kotlin/android/content/Context) returned by calling -> [`ProjectedContext.createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)). -> ->
-> -
- During the permissions user flow, here is what your app and the user can expect: 1. **On the glasses** : An activity appears on the **projected device @@ -174,5 +139,75 @@ During the permissions user flow, here is what your app and the user can expect: 4. **Receiving the result** : After the user makes their final choice, the activities on both the phone and glasses are dismissed. Your - [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map containing - the granted status for each requested permission. \ No newline at end of file + [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map + containing the granted status for each requested permission. + +### Request runtime permissions in a phone activity + +If your app is running in a phone activity but requires permissions for audio or +display glasses---for example, to let a user switch a video stream to the glasses' +camera for a first-person point of view---request the permissions using the +device-specific [`Activity#requestPermissions(permissions, requestCode, +deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1) method. + +To target audio and display glasses, obtain the appropriate device ID by calling +[`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) on a projected device context. Pass this ID when requesting +permissions from your phone activity, as shown in the following example: + + +```kotlin +// Request the projected permission from phone activity +requestPermissions( + arrayOf(Manifest.permission.CAMERA), + // REQUEST_CODE_GLASSES_CAMERA is a developer-defined constant + REQUEST_CODE_GLASSES_CAMERA, + projectedDeviceId +) +``` + +
+ +#### Handle the permission results + +Once the user responds to the permission dialog on the phone, their decision is +delivered to your app by invoking the onRequestPermissionsResult callback. + +To handle the response, override [`onRequestPermissionsResult`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int,java.lang.String%5B%5D,int%5B%5D)) within the +`Activity` instance that initiated the permission request. + +The following code snippet shows how to handle the callback and verify whether +the user granted the requested permissions: + + +```kotlin +private companion object { + // REQUEST_CODE_GLASSES_CAMERA is a developer-defined constant. + const val REQUEST_CODE_GLASSES_CAMERA = 1001 +} + +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + deviceId: Int +) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults, deviceId) + + // Handle the result of the permission request + if (requestCode == REQUEST_CODE_GLASSES_CAMERA && deviceId == projectedDeviceId) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Proceed with glasses camera features + } else { + // Handle glasses permission denied + } + } +} +``` + +
+ +##### Key points about the code + +- Use the `deviceId` overload with the `onRequestPermissionsResult` callback to ensure the permission status is correctly mapped to the specific context, such as the audio or display glasses rather than the host phone. +- Your app should handle both granted and denied results gracefully. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md deleted file mode 100644 index 1b8ad16..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md +++ /dev/null @@ -1,389 +0,0 @@ -When creating a Glimmer Icon component, refer to the following source code in -`Icon.kt`: - - -```kotlin -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.xr.glimmer - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.structuralEqualityPolicy -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.CacheDrawModifierNode -import androidx.compose.ui.draw.paint -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorProducer -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toolingGraphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.node.DelegatingNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.invalidateDraw -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.Dp - -/** - * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a - * content color tint. Icon is an opinionated component designed to be used with single-color icons - * so that they can be tinted correctly for the component they are placed in. The recommended icon - * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly - * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons - * inside the component. The content color used to tint this icon is provided by [surface]. To set a - * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons - * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For - * generic images that should not be tinted, and should not use the provided icon size, use the - * generic [androidx.compose.foundation.Image] instead. - * - * @param imageVector [ImageVector] to draw inside this icon - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon( - imageVector: ImageVector, - contentDescription: String?, - modifier: Modifier = Modifier, -) { - Icon( - painter = rememberVectorPainter(imageVector), - contentDescription = contentDescription, - modifier = modifier, - ) -} - -/** - * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a - * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so - * that they can be tinted correctly for the component they are placed in. The recommended icon - * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly - * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons - * inside the component. Use the other overload of Icon without a [tint] parameter to apply the - * recommended content color provided by a [surface]. For multicolored icons and icons that should - * not be tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, - * and should not use the provided icon size, use the generic [androidx.compose.foundation.Image] - * instead. - * - * @param imageVector [ImageVector] to draw inside this icon - * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no tint - * is applied. - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon( - imageVector: ImageVector, - tint: Color, - contentDescription: String?, - modifier: Modifier = Modifier, -) { - Icon( - painter = rememberVectorPainter(imageVector), - tint = - if (tint.isSpecified) { - { tint } - } else null, - contentDescription = contentDescription, - modifier = modifier, - ) -} - -/** - * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a - * content color tint. Icon is an opinionated component designed to be used with single-color icons - * so that they can be tinted correctly for the component they are placed in. The recommended icon - * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly - * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons - * inside the component. The content color used to tint this icon is provided by [surface]. To set a - * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons - * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For - * generic images that should not be tinted, and should not use the provided icon size, use the - * generic [androidx.compose.foundation.Image] instead. - * - * @param bitmap [ImageBitmap] to draw inside this icon - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon(bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier) { - val painter = remember(bitmap) { BitmapPainter(bitmap) } - Icon(painter = painter, contentDescription = contentDescription, modifier = modifier) -} - -/** - * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a tint - * of [tint]. Icon is an opinionated component designed to be used with single-color icons so that - * they can be tinted correctly for the component they are placed in. The recommended icon sizes can - * be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly using [size], - * and components can use [LocalIconSize] to set the preferred size for any Icons inside the - * component. Use the other overload of Icon without a [tint] parameter to apply the recommended - * content color provided by a [surface]. For multicolored icons and icons that should not be - * tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, and - * should not use the provided icon size, use the generic [androidx.compose.foundation.Image] - * instead. - * - * @param bitmap [ImageBitmap] to draw inside this icon - * @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no tint is - * applied. - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon( - bitmap: ImageBitmap, - tint: Color, - contentDescription: String?, - modifier: Modifier = Modifier, -) { - val painter = remember(bitmap) { BitmapPainter(bitmap) } - Icon( - painter = painter, - tint = - if (tint.isSpecified) { - { tint } - } else null, - contentDescription = contentDescription, - modifier = modifier, - ) -} - -/** - * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a - * content color tint. Icon is an opinionated component designed to be used with single-color icons - * so that they can be tinted correctly for the component they are placed in. The recommended icon - * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly - * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons - * inside the component. The content color used to tint this icon is provided by [surface]. To set a - * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons - * that should not be tinted, use the overload and set `null` for the tint parameter. For generic - * images that should not be tinted, and should not use the provided icon size, use the generic - * [androidx.compose.foundation.Image] instead. - * - * @param painter [Painter] to draw inside this icon - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon(painter: Painter, contentDescription: String?, modifier: Modifier = Modifier) { - Icon( - painter = painter, - useContentColor = true, - tint = null, - contentDescription = contentDescription, - modifier = modifier, - ) -} - -/** - * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a - * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so - * that they can be tinted correctly for the component they are placed in. The recommended icon - * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly - * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons - * inside the component. Use the other overload of Icon without a [tint] parameter to apply the - * recommended content color provided by a [surface]. For multicolored icons and icons that should - * not be tinted, set [tint] to `null`. For generic images that should not be tinted, and should not - * use the provided icon size, use the generic [androidx.compose.foundation.Image] instead. - * - * @param painter [Painter] to draw inside this icon - * @param tint tint to be applied to [painter]. If null, then no tint is applied. - * @param contentDescription text used by accessibility services to describe what this icon - * represents. This should always be provided unless this icon is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be localized, - * such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier the [Modifier] to be applied to this icon - * @see LocalIconSize - * @see GlimmerTheme.Companion.iconSizes - */ -@Composable -public fun Icon( - painter: Painter, - tint: ColorProducer?, - contentDescription: String?, - modifier: Modifier = Modifier, -) { - Icon( - painter = painter, - useContentColor = false, - tint = tint, - contentDescription = contentDescription, - modifier = modifier, - ) -} - -/** - * CompositionLocal containing the preferred size of an icon. This value will be used by [Icon] by - * default - it can be overridden with a [size] modifier. - */ -public val LocalIconSize: ProvidableCompositionLocal = - compositionLocalOf(structuralEqualityPolicy()) { GlimmerTheme._iconSizes.medium } - -@Composable -private fun Icon( - painter: Painter, - useContentColor: Boolean, - tint: ColorProducer?, - contentDescription: String?, - modifier: Modifier, -) { - val colorFilter = - if (useContentColor || tint != null) { - IconColorFilterElement( - useContentColor = useContentColor, - tint = tint, - painter = painter, - ) - } else { - Modifier - } - val semantics = - if (contentDescription != null) { - Modifier.semantics { - this.contentDescription = contentDescription - this.role = Role.Image - } - } else { - Modifier - } - Box( - modifier - .toolingGraphicsLayer() - .size(LocalIconSize.current) - .then(colorFilter) - .paint(painter, contentScale = ContentScale.Fit) - .then(semantics) - ) -} - -@Suppress("ModifierNodeInspectableProperties") -private class IconColorFilterElement( - private val useContentColor: Boolean, - private val tint: ColorProducer?, - private val painter: Painter, -) : ModifierNodeElement() { - override fun create() = IconColorFilterNode(useContentColor, tint, painter) - - override fun update(node: IconColorFilterNode) { - node.update(useContentColor, tint, painter) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is IconColorFilterElement) return false - - if (useContentColor != other.useContentColor) return false - if (tint != other.tint) return false - if (painter != other.painter) return false - - return true - } - - override fun hashCode(): Int { - var result = useContentColor.hashCode() - result = 31 * result + (tint?.hashCode() ?: 0) - result = 31 * result + painter.hashCode() - return result - } -} - -private class IconColorFilterNode( - private var useContentColor: Boolean, - private var tint: ColorProducer?, - private var painter: Painter, -) : DelegatingNode() { - val cacheDrawNode = - delegate( - CacheDrawModifierNode { - val layer = obtainGraphicsLayer() - layer.apply { record { drawContent() } } - var cachedTintColor = Color.Unspecified - var cachedColorFilter: ColorFilter? = null - onDrawWithContent { - val tintColor = - if (useContentColor) { - currentContentColor() - } else { - tint?.invoke() ?: Color.Unspecified - } - if (cachedTintColor != tintColor) { - cachedTintColor = tintColor - cachedColorFilter = - if (tintColor.isSpecified) ColorFilter.tint(tintColor) else null - layer.colorFilter = cachedColorFilter - } - drawLayer(graphicsLayer = layer) - } - } - ) - - fun update(useContentColor: Boolean, tint: ColorProducer?, painter: Painter) { - val painterChanged = this.painter != painter - if (this.useContentColor != useContentColor || this.tint != tint || painterChanged) { - this.useContentColor = useContentColor - this.tint = tint - this.painter = painter - if (painterChanged) { - cacheDrawNode.invalidateDrawCache() - } else { - cacheDrawNode.invalidateDraw() - } - } - } -} -``` - -
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md deleted file mode 100644 index 0f199a2..0000000 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md +++ /dev/null @@ -1,281 +0,0 @@ -When creating a Glimmer Icon Sizes component, refer to the following source -code in `ListItem.kt` for list items: - - -```kotlin -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.xr.glimmer - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp - -/** - * ListItem is a component used to represent a single item in a - * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may - * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting - * label is displayed below the primary label and can be used to provide additional information. A - * ListItem fills the maximum width available by default. - * - * This ListItem is focusable - see the other [ListItem] overload for a clickable ListItem. - * - * A simple ListItem with just a primary label: - * - * @sample androidx.xr.glimmer.samples.ListItemSample - * - * A ListItem with a primary and supporting label: - * - * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelSample - * - * A ListItem with a primary label, a supporting label, and a leading icon: - * - * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelAndLeadingIconSample - * @param modifier the [Modifier] to be applied to this list item - * @param supportingLabel optional supporting label to be placed underneath the primary label - * [content] - * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is - * typically an [Icon] tinted with [contentColor] by default. - * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This - * is typically an [Icon] tinted with [contentColor] by default. - * @param shape the [Shape] used to clip this list item, and also used to draw the background and - * border - * @param color background color of this list item - * @param contentColor content color used by components inside [content], [supportingLabel], - * [leadingIcon], and [trailingIcon]. - * @param border the border to draw around this list item - * @param contentPadding the spacing values to apply internally between the container and the - * content - * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and - * emitting Interactions for this list item. You can use this to change the list item's appearance - * or preview the list item in different states. Note that if `null` is provided, interactions - * will still happen internally. - * @param content the main content / primary label to display inside this list item - */ -@Composable -public fun ListItem( - modifier: Modifier = Modifier, - supportingLabel: @Composable (() -> Unit)? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - shape: Shape = GlimmerTheme.shapes.medium, - color: Color = GlimmerTheme.colors.surface, - contentColor: Color = calculateContentColor(color), - border: BorderStroke? = SurfaceDefaults.border(), - contentPadding: PaddingValues = ListItemDefaults.contentPadding, - interactionSource: MutableInteractionSource? = null, - content: @Composable () -> Unit, -) { - ListItemImpl( - modifier = modifier, - onClick = null, - supportingLabel = supportingLabel, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - shape = shape, - color = color, - contentColor = contentColor, - border = border, - contentPadding = contentPadding, - interactionSource = interactionSource, - content = content, - ) -} - -/** - * ListItem is a component used to represent a single item in a - * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may - * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting - * label is displayed below the primary label and can be used to provide additional information. A - * ListItem fills the maximum width available by default. - * - * This ListItem is focusable and clickable - see the other [ListItem] overload for a ListItem that - * is only focusable. - * - * A simple clickable ListItem with just a primary label: - * - * @sample androidx.xr.glimmer.samples.ClickableListItemSample - * - * A clickable ListItem with a primary and supporting label: - * - * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelSample - * - * A clickable ListItem with a primary label, a supporting label, and a leading icon: - * - * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelAndLeadingIconSample - * @param onClick called when this list item is clicked - * @param modifier the [Modifier] to be applied to this list item - * @param supportingLabel optional supporting label to be placed underneath the primary label - * [content] - * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is - * typically an [Icon] tinted with [contentColor] by default. - * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This - * is typically an [Icon] tinted with [contentColor] by default. - * @param shape the [Shape] used to clip this list item, and also used to draw the background and - * border - * @param color background color of this list item - * @param contentColor content color used by components inside [content], [supportingLabel], - * [leadingIcon], and [trailingIcon]. - * @param border the border to draw around this list item - * @param contentPadding the spacing values to apply internally between the container and the - * content - * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and - * emitting Interactions for this list item. You can use this to change the list item's appearance - * or preview the list item in different states. Note that if `null` is provided, interactions - * will still happen internally. - * @param content the main content / primary label to display inside this list item - */ -@Composable -public fun ListItem( - onClick: () -> Unit, - modifier: Modifier = Modifier, - supportingLabel: @Composable (() -> Unit)? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - shape: Shape = GlimmerTheme.shapes.medium, - color: Color = GlimmerTheme.colors.surface, - contentColor: Color = calculateContentColor(color), - border: BorderStroke? = SurfaceDefaults.border(), - contentPadding: PaddingValues = ListItemDefaults.contentPadding, - interactionSource: MutableInteractionSource? = null, - content: @Composable () -> Unit, -) { - ListItemImpl( - modifier = modifier, - onClick = onClick, - supportingLabel = supportingLabel, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - shape = shape, - color = color, - contentColor = contentColor, - border = border, - contentPadding = contentPadding, - interactionSource = interactionSource, - content = content, - ) -} - -@Composable -private fun ListItemImpl( - modifier: Modifier, - onClick: (() -> Unit)?, - supportingLabel: @Composable (() -> Unit)?, - leadingIcon: @Composable (() -> Unit)?, - trailingIcon: @Composable (() -> Unit)?, - shape: Shape, - color: Color, - contentColor: Color, - border: BorderStroke?, - contentPadding: PaddingValues, - interactionSource: MutableInteractionSource?, - content: @Composable () -> Unit, -) { - val iconSize = GlimmerTheme.iconSizes.large - val typography = GlimmerTheme.typography - val innerPadding = GlimmerTheme.componentSpacingValues.small - val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } - - val surfaceModifier = - Modifier.surface( - shape = shape, - color = color, - contentColor = contentColor, - // TODO(b/532516157): Reenable depthEffect after b/446294492 is fixed. - depthEffect = null, - border = border, - interactionSource = internalInteractionSource, - ) - .then( - if (onClick != null) { - Modifier.clickable( - interactionSource = internalInteractionSource, - onClick = onClick, - ) - } else { - Modifier.focusable(interactionSource = internalInteractionSource) - } - ) - - Row( - modifier = - modifier - .then(surfaceModifier) - .fillMaxWidth() - .defaultMinSize(minHeight = MinimumHeight) - .padding(contentPadding), - verticalAlignment = CenterVertically, - ) { - if (leadingIcon != null) { - Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopStart) { - CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) - } - } - Column(Modifier.weight(1f).padding(horizontal = innerPadding)) { - if (supportingLabel == null) { - CompositionLocalProvider( - LocalTextStyle provides typography.bodySmall, - content = content, - ) - } else { - CompositionLocalProvider( - LocalTextStyle provides typography.titleSmall, - content = content, - ) - CompositionLocalProvider( - LocalTextStyle provides typography.bodySmall, - content = supportingLabel, - ) - } - } - if (trailingIcon != null) { - Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopEnd) { - CompositionLocalProvider(LocalIconSize provides iconSize, content = trailingIcon) - } - } - } -} - -/** Default values used for [ListItem] */ -public object ListItemDefaults { - /** Default content padding used for a [ListItem] */ - public val contentPadding: PaddingValues - @Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.large) -} - -/** Default minimum height for a [ListItem] */ -private val MinimumHeight = 80.dp -``` - -
\ No newline at end of file From 28822b2306f34740542e4150888c5b0cde41724d Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:20:39 +0000 Subject: [PATCH 40/61] Revert last update (#126) --- .claude-plugin/marketplace.json | 1 + .codex-plugin/plugin.json | 5 + device-ai/appfunctions/SKILL.md | 6 +- .../appfunctions/{references => }/context.md | 0 identity/verified-email/SKILL.md | 14 +- .../email-verification-implementation.md | 24 +- .../digital-credentials/email-verification.md | 145 ++ .../identity/passkeys/create-passkeys.md | 8 +- jetpack-compose/adaptive/SKILL.md | 2 +- .../layouts/adaptive/flexbox/get-started.md | 2 +- .../layouts/adaptive/grid/get-started.md | 2 +- performance/r8-analyzer/SKILL.md | 37 +- .../references/CONFIGURATION-ANALYZER.md | 287 ++++ .../references/REFLECTION-GUIDE.md | 139 ++ play/engage-sdk-integration/SKILL.md | 8 +- .../android/guide/playcore/engage/social.md | 702 ---------- .../playcore/engage/tv/getting-started.md | 16 +- .../references/common.md | 519 +++++++ .../SKILL.md | 93 ++ .../google/play/billing/release-notes.md | 1226 +++++++++++++++++ .../references/migration-logic.md | 78 ++ .../references/version-checklist.md | 54 + wear/wear-compose-m3/SKILL.md | 165 +-- .../wearables/compose/migrate-to-material3.md | 10 +- .../foundation/CurvedWorldSample.kt.md.txt | 430 ------ .../foundation/ExpandableSample.kt.md.txt | 99 -- .../HierarchicalFocusSample.kt.md.txt | 141 -- .../foundation/PagerSamples.kt.md.txt | 56 - .../foundation/RotarySamples.kt.md.txt | 153 -- .../ScalingLazyColumnSample.kt.md.txt | 131 -- .../SwipeToDismissBoxSample.kt.md.txt | 168 --- .../foundation/SwipeToRevealSample.kt.md.txt | 282 ---- .../TransformingLazyColumnSample.kt.md.txt | 335 ----- .../material3/AlertDialogSample.kt.md.txt | 377 ----- .../material3/AnimatedTextSample.kt.md.txt | 165 --- .../material3/ButtonGroupSample.kt.md.txt | 91 -- .../material3/ButtonSample.kt.md.txt | 496 ------- .../references/material3/CardSample.kt.md.txt | 470 ------- .../material3/CheckboxButtonSample.kt.md.txt | 69 - .../ConfirmationDialogSample.kt.md.txt | 162 --- .../material3/CurvedTextSamples.kt.md.txt | 78 -- .../material3/DatePickerSample.kt.md.txt | 131 -- .../DynamicColorSchemeSample.kt.md.txt | 74 - .../material3/EdgeButtonSample.kt.md.txt | 63 - .../FadingExpandingLabelSample.kt.md.txt | 60 - .../material3/IconButtonSample.kt.md.txt | 125 -- .../IconToggleButtonSample.kt.md.txt | 116 -- .../material3/LevelIndicatorSample.kt.md.txt | 68 - .../material3/ListHeaderSample.kt.md.txt | 177 --- .../OneHandedGestureSamples.kt.md.txt | 651 --------- .../OpenOnPhoneDialogSample.kt.md.txt | 57 - .../material3/PageIndicatorSample.kt.md.txt | 110 -- .../material3/PagerScaffoldSample.kt.md.txt | 204 --- .../material3/PickerSample.kt.md.txt | 111 -- .../material3/PlaceholderSample.kt.md.txt | 182 --- .../ProgressIndicatorSample.kt.md.txt | 263 ---- .../material3/RadioButtonSample.kt.md.txt | 109 -- .../material3/ScaffoldSample.kt.md.txt | 146 -- .../material3/ScrollAwaySample.kt.md.txt | 97 -- .../material3/ScrollIndicatorSample.kt.md.txt | 66 - .../material3/SliderSample.kt.md.txt | 86 -- .../material3/StepperSample.kt.md.txt | 148 -- .../SurfaceTransformationSample.kt.md.txt | 165 --- .../SwipeToDismissBoxSample.kt.md.txt | 171 --- .../material3/SwitchButtonSample.kt.md.txt | 68 - .../material3/TextButtonSample.kt.md.txt | 115 -- .../TextToggleButtonSample.kt.md.txt | 71 - .../material3/TimeTextSample.kt.md.txt | 65 - .../TransformationSpecSample.kt.md.txt | 246 ---- ...ingLazyColumnNotificationsSample.kt.md.txt | 89 -- .../TransformingLazyColumnSample.kt.md.txt | 309 ----- .../SKILL.md | 8 +- .../request-hardware-permissions.md | 179 +-- .../references/icon-source.md | 389 ++++++ .../references/listitem-source.md | 281 ++++ 75 files changed, 3403 insertions(+), 9043 deletions(-) rename device-ai/appfunctions/{references => }/context.md (100%) create mode 100644 identity/verified-email/references/android/identity/digital-credentials/email-verification.md create mode 100644 performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md create mode 100644 performance/r8-analyzer/references/REFLECTION-GUIDE.md delete mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/social.md create mode 100644 play/engage-sdk-integration/references/common.md create mode 100644 play/play-billing-library-version-upgrade/SKILL.md create mode 100644 play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md create mode 100644 play/play-billing-library-version-upgrade/references/migration-logic.md create mode 100644 play/play-billing-library-version-upgrade/references/version-checklist.md delete mode 100644 wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/CardSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt delete mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt create mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md create mode 100644 xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fa5e1e5..d8900b3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -25,6 +25,7 @@ "./navigation/navigation-3", "./performance/r8-analyzer", "./play/engage-sdk-integration", + "./play/play-billing-library-version-upgrade", "./play/play-policy-insights", "./profilers/perfetto-sql", "./profilers/perfetto-trace-analysis", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 637f0b7..f28a37b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -70,6 +70,11 @@ "path": "./play/engage-sdk-integration" } }, + { + "source": { + "path": "./play/play-billing-library-version-upgrade" + } + }, { "source": { "path": "./play/play-policy-insights" diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md index cc5a2c6..ad75915 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-07-31' + last-updated: '2026-07-28' keywords: - AppFunctions - Kotlin @@ -55,7 +55,7 @@ If users request a subset of steps, you must encourage them to use all steps. If they apply, you must load the following references: -- *[Context and terminology](references/context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. +- *[Context and terminology](context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. - *[Migration to service entry point](references/migrate-to-service-entry-point.md)* : Documents the systematic procedure for migrating Android applications using AppFunctions versions 1.0.0-alpha09 and lower to `AppFunctionServiceEntryPoint` architecture introduced in version 1.0.0-alpha10. Load when a user asks to migrate or upgrade existing AppFunctions code, or when you encounter legacy `AppFunctionConfiguration.Provider` implementations. ## Critical constraints @@ -67,5 +67,5 @@ If they apply, you must load the following references: - If you encounter build-time errors such as Kotlin Symbol Processing (KSP) issues, see [Implementation and configuration](references/implementation-configuration.md). - If you encounter runtime errors such as missing services or execution failures, see [Testing and debugging](references/adb-interaction-testing.md). -- If you need architecture definitions and vocabulary, see [Context and terminology](references/context.md). +- If you need architecture definitions and vocabulary, see [Context and terminology](context.md). - If you encounter issues when upgrading legacy configurations, see [Migration to service entry point](references/migrate-to-service-entry-point.md). diff --git a/device-ai/appfunctions/references/context.md b/device-ai/appfunctions/context.md similarity index 100% rename from device-ai/appfunctions/references/context.md rename to device-ai/appfunctions/context.md diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index b331f3e..784038e 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-07-30' + last-updated: '2026-07-02' keywords: - implementation - Android @@ -26,7 +26,7 @@ metadata: ## Fundamentals -- *[Overview of Digital Credentials](https://developer.android.com/identity/digital-credentials/email-verification)*: Learn about cryptographically verifiable documents and the role of Credential Manager. +- *[Overview of Digital Credentials](references/android/identity/digital-credentials/email-verification.md)*: Learn about cryptographically verifiable documents and the role of Credential Manager. - *[Glossary](references/android/identity/digital-credentials/email-verification-implementation.md)* : Definitions for `dcql_query`, `UserInfoCredential`, and `GetDigitalCredentialOption`. ### Standards \& Examples @@ -130,8 +130,8 @@ Manager: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha03") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") + implementation("androidx.credentials:credentials:1.7.0-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") } ``` @@ -139,8 +139,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha03" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" } ``` @@ -404,7 +404,7 @@ actual call to the Credential Manager API. ## See also -- [Overview of verified email retrieval](https://developer.android.com/identity/digital-credentials/email-verification) +- [Overview of verified email retrieval](references/android/identity/digital-credentials/email-verification.md) - [Credential Manager](references/android/identity/credential-manager/index.md) ## Critical Security Guidelines 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 cb4bcc4..9305e92 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,16 +1,6 @@ -## 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: - - android skills add --skill verified-email - -
- -## Overview +> [!IMPORTANT] +> **Important:** We released an agent skill to help you implement email verification with the Digital Credentials API. Try out the skill from the [Android skills +> repository](https://github.com/android/skills). 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 @@ -25,8 +15,8 @@ Manager: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha03") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") + implementation("androidx.credentials:credentials:1.7.0-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") } ``` @@ -34,8 +24,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha03" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" } ``` 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 new file mode 100644 index 0000000..2134577 --- /dev/null +++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md @@ -0,0 +1,145 @@ +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: + +- Android compatibility +- User experience +- Accounts supported +- Implication of email deliverability +- Comparison with [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) + +This guide assumes you are familiar with the following concepts: + +- [Credential Manager](https://developer.android.com/identity/credential-manager) +- [Digital Credentials](https://developer.android.com/identity/digital-credentials) +- [Verifiable Credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) + +## 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. + +## 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 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. + +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. + + > [!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. + + > [!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: + +- **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. + +#### 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. +![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. +![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. +![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. + +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). + +> [!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. +> For phone number verification, see [Verify phone numbers with digital credentials](https://developer.android.com/identity/digital-credentials/phone-number-verification). For a Firebase solution, see [Firebase Phone Number Verification](https://firebase.google.com/docs/phone-number-verification). Note that you can't verify a phone number and email in the same invocation as these are separate capabilities. + +## 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. + +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. + +### 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: + +- **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. +- **Platform support**: The Credential Manager flow is an Android-only solution. +- **Scopes** : Unlike Sign in with Google, which can use OAuth 2.0 to request access to user data (such as Calendar or Drive through scopes), the Digital Credentials API is strictly for retrieving verified identity attributes. It cannot be used to request additional [authorization scopes](https://developers.google.com/identity/protocols/oauth2/scopes). + +## Next steps + +To implement this feature in your app, see the [Implementation guide](https://developer.android.com/identity/digital-credentials/email-verification-implementation). \ No newline at end of file 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 089cc21..bfe19e5 100644 --- a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -44,8 +44,8 @@ Add the following dependencies to your app module's `build.gradle` file: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha03") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") + implementation("androidx.credentials:credentials:1.7.0-alpha02") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") } ``` @@ -53,8 +53,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha03" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" + implementation "androidx.credentials:credentials:1.7.0-alpha02" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" } ``` diff --git a/jetpack-compose/adaptive/SKILL.md b/jetpack-compose/adaptive/SKILL.md index d78cc17..566374a 100644 --- a/jetpack-compose/adaptive/SKILL.md +++ b/jetpack-compose/adaptive/SKILL.md @@ -10,7 +10,7 @@ description: Instructions to make or update an app's UI so that it adapts to dif license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-30' + last-updated: '2026-07-02' keywords: - android - ui 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 b7b2763..8b00b01 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 @@ -6,7 +6,7 @@ This page describes how to implement basic `FlexBox` layouts. `lib.versions.toml`. [versions] - compose = "1.12.0-rc01" + compose = "1.12.0-beta02" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } 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 b4d7eeb..bb78a97 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 @@ -6,7 +6,7 @@ This page describes how to implement basic [`Grid`](https://developer.android.co `lib.versions.toml`. [versions] - compose = "1.12.0-rc01" + compose = "1.12.0-beta02" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index 027793d..2812549 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-01' + last-updated: '2026-07-18' keywords: - R8 - proguard @@ -20,40 +20,33 @@ metadata: - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. - Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. -- **AGP** : If version is lower than 9.0, suggest migration to 9.0 for [build-time performance improvement](references/android/topic/performance/app-optimization/enable-app-optimization.md). +- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement + performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) - **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. ## 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. + `libs.versions.toml` to get the R8 version -- **If AGP \>= 9.3.0** : Proceed to **Path A (Standalone Task)**. +- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. -- **If AGP \< 9.3.0 and R8 \>= 9.3.7-dev** : Proceed to **Path B (Quantitative)**. +- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. -- If none of the conditions are met, proceed to **Path C (Heuristic)**. +### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) -### Path A: Standalone Gradle task (AGP \>= 9.3.0) +- **Check requirements** : Python and `protobuf` package are mandatory. +- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. +- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. -- **Step 1: Run standalone task** : Run `./gradlew :app:analyzeReleaseR8Config` to evaluate the R8 configuration. You MUST wait for this command to finish before proceeding. -- **Step 2: Convert to JSON** : The report is generated at `app/build/reports/r8/r8-config-analyzer-release.pb`. You MUST explicitly run the conversion script by executing: `python3 .agents/skills/r8-analyzer/scripts/convert_pb_to_json.py`. Wait for this command to finish. -- **Step 3: Analyze** : You MUST explicitly run the analysis script by executing: `python3 .agents/skills/r8-analyzer/scripts/analyze.py`. This outputs `tmp/keepradius/analysis_result.txt`. Wait for this command to finish. - -### Path B: Quantitative data generation (R8 \>= 9.3.7-dev and AGP \< 9.3.0) - -- **Step 1: Check requirements** : Python and `protobuf` package are mandatory. -- **Step 2: Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](https://developer.android.com/agents/skills/performance/r8-analyzer/references/CONFIGURATION-ANALYZER) to generate the proto file using R8 configuration analyzer, convert it to JSON and analyze the result. -- **Step 3: Analyze** : You MUST ensure the analysis produces `tmp/keepradius/analysis_result.txt` for scores and rule impact metrics. - -### Path C: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) +### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) *(Use ONLY if quantitative data generation is not possible)* -- **Step 1: Manual evaluation** : Inspect `proguard-rules.pro`. -- **Step 2: Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. -- **Step 3: Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](https://developer.android.com/agents/skills/performance/r8-analyzer/references/REFLECTION-GUIDE) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). -- **Step 4: Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. +- **Manual evaluation** : Inspect `proguard-rules.pro`. +- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. +- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). +- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. ## Step 3. Report generation diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md new file mode 100644 index 0000000..4039dcf --- /dev/null +++ b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -0,0 +1,287 @@ +On each step, keep the user informed of the progress by displaying the output. + +### 1. Requirements + +- R8 Version: 9.3.7-dev or later + +### 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: + + 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: + + # Remove the intermediate JSON and the directory containing protobuf files + rm tmp/r8analysis/keepruleradius.json + rm tmp/r8analysis/*.pb + + # Copy the previous result to history before deleting the analysis + if [ -f tmp/r8analysis/analysis_result.txt ]; + then cat tmp/r8analysis/analysis_result.txt > tmp/r8analysis/history.txt && + rm tmp/r8analysis/analysis_result.txt; fi + +### 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 + + ./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. + + import sys + import os + import glob + from google.protobuf import json_format + import keep_radius_pb2 + + def convert_pb_to_json(input_pb_path, output_json_path): + bundle = keep_radius_pb2.BlastRadiusContainer() + + try: + with open(input_pb_path, "rb") as pb_file: + binary_data = pb_file.read() + except Exception as e: + print(f"Error reading file {input_pb_path}: {e}", file=sys.stderr) + return False + + try: + bundle.ParseFromString(binary_data) + except Exception as e: + print(f"Error parsing protobuf: {e}", file=sys.stderr) + return False + + try: + json_string = json_format.MessageToJson( + bundle, + always_print_fields_with_no_presence=True, + preserving_proto_field_name=True, + indent=4 + ) + with open(output_json_path, "w", encoding="utf-8") as json_file: + json_file.write(json_string) + return True + except Exception as e: + print(f"Error writing JSON: {e}", file=sys.stderr) + return False + + if __name__ == "__main__": + input_pb = sys.argv[1] if len(sys.argv) > 1 else None + if not input_pb: + pb_files = glob.glob("tmp/r8analysis/*.pb") + if not pb_files: + print("Error: No .pb file found in tmp/r8analysis", file=sys.stderr) + sys.exit(1) + input_pb = sorted(pb_files)[-1] # Use the most recent one + output_json = sys.argv[2] if len(sys.argv) > 2 else "tmp/r8analysis/keepruleradius.json" + if not convert_pb_to_json(input_pb, output_json): + sys.exit(1) + +### 6. Analyze + +Run the following analysis script on the generated JSON to get the impact of the +keep rules and sort it. + + import json, sys + + def analyze(path): + try: + with open(path, 'r') as f: + d = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Build reference map + c_map = {c.get('id'): set(c.get('constraints', [])) for c in d.get('keep_constraints_table', [])} + r_map = {r.get('id'): c_map.get(r.get('constraints_id'), set()) for r in d.get('keep_rule_blast_radius_table', [])} + + tot_opt = tot_obf = tot_shr = tot_items = 0 + + # Tally constraints across all kept items + for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table'): + for i in d.get(tbl, []): + tot_items += 1 + kb = i.get('kept_by', []) + if any('DONT_OPTIMIZE' in r_map.get(r, set()) for r in kb): tot_opt += 1 + if any('DONT_OBFUSCATE' in r_map.get(r, set()) for r in kb): tot_obf += 1 + if any('DONT_SHRINK' in r_map.get(r, set()) for r in kb): tot_shr += 1 + + # Find denominator + bi = d.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else tot_items + + # Check for globals + globals_src = [g.get('source', '').lower() for g in d.get('global_keep_rule_blast_radius_table', [])] + def score(cnt, flag): + if any(flag in src for src in globals_src): return 0.0 + return max(0.0, 100.0 - ((cnt / denom * 100) if denom > 0 else 0)) + + result = [ + f"Optimization Score: {score(tot_opt, '-dontoptimize'):.2f}%", + f"Obfuscation Score: {score(tot_obf, '-dontobfuscate'):.2f}%", + f"Shrinking Score: {score(tot_shr, '-dontshrink'):.2f}%" + ] + for line in result: + print(line) + with open("tmp/r8analysis/analysis_result.txt", "w") as f: + f.write("\n".join(result)) + + if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "tmp/r8analysis/keepruleradius.json" + analyze(path) + +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. + + import json, sys + + def report(path): + try: + with open(path, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + return + + # Calculate denominator for percentage + bi = data.get('build_info', {}) + live = sum(int(bi.get(k, 0)) for k in ('live_class_count', 'live_field_count', 'live_method_count')) + denom = live if live > 0 else sum(len(data.get(tbl, [])) for tbl in ('kept_class_info_table', 'kept_field_info_table', 'kept_method_info_table')) + + processed = [] + for r in data.get('keep_rule_blast_radius_table', []): + br = r.get('blast_radius', {}) + c, f, m = len(br.get('class_blast_radius', [])), len(br.get('field_blast_radius', [])), len(br.get('method_blast_radius', [])) + impact = c + f + m + if impact == 0: + continue + impact_pct = (impact / denom * 100) if denom > 0 else 0.0 + processed.append({ + 'id': r.get('id'), + 'source': r.get('source'), + 'impact': impact, + 'impact_pct': f"{impact_pct:.2f}%", + 'classes': c, + 'fields': f, + 'methods': m, + 'subsumed_by': br.get('subsumed_by', []) + }) + + processed.sort(key=lambda x: x['impact'], reverse=True) + + # Output JSON for the agent to fetch and process + print(json.dumps({ + "top_5_impact_keep_rules": [r for r in processed if not r['subsumed_by']][:5], + "subsumed": [r for r in processed if r['subsumed_by']] + }, indent=2)) + + if __name__ == "__main__": + report("tmp/r8analysis/keepruleradius.json") + +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 + +### 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 + + 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. + + from google.protobuf import descriptor as _descriptor + from google.protobuf import descriptor_pool as _descriptor_pool + from google.protobuf import runtime_version as _runtime_version + from google.protobuf import symbol_database as _symbol_database + from google.protobuf.internal import builder as _builder + _runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 4, + '', + 'keep_radius.proto' + ) + # @@protoc_insertion_point(imports) + + _sym_db = _symbol_database.Default() + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11keep_radius.proto\x12&com.android.tools.r8.blastradius.proto\"\x9f\x02\n\x13KeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x16\n\x0e\x63onstraints_id\x18\x03 \x01(\x05\x12\x46\n\x06origin\x18\x04 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\x12I\n\x0c\x62last_radius\x18\x05 \x01(\x0b\x32\x33.com.android.tools.r8.blastradius.proto.BlastRadius\x12\x41\n\x04tags\x18\x06 \x03(\x0e\x32\x33.com.android.tools.r8.blastradius.proto.KeepRuleTag\"w\n\x0b\x42lastRadius\x12\x13\n\x0bsubsumed_by\x18\x01 \x03(\x05\x12\x1a\n\x12\x63lass_blast_radius\x18\x02 \x03(\x05\x12\x1a\n\x12\x66ield_blast_radius\x18\x03 \x03(\x05\x12\x1b\n\x13method_blast_radius\x18\x04 \x03(\x05\"\x7f\n\x19GlobalKeepRuleBlastRadius\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x46\n\x06origin\x18\x03 \x01(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.TextFileOrigin\"j\n\x0fKeepConstraints\x12\n\n\x02id\x18\x01 \x01(\x05\x12K\n\x0b\x63onstraints\x18\x02 \x03(\x0e\x32\x36.com.android.tools.r8.blastradius.proto.KeepConstraint\"`\n\rKeptClassInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"`\n\rKeptFieldInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x66ield_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"b\n\x0eKeptMethodInfo\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1b\n\x13method_reference_id\x18\x02 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x03 \x01(\x05\x12\x0f\n\x07kept_by\x18\x04 \x03(\x05\"a\n\x0e\x46ieldReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x19\n\x11type_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"c\n\x0fMethodReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12\x63lass_reference_id\x18\x02 \x01(\x05\x12\x1a\n\x12proto_reference_id\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\"K\n\x0eProtoReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x15\n\rparameters_id\x18\x02 \x01(\x05\x12\x16\n\x0ereturn_type_id\x18\x03 \x01(\x05\"4\n\rTypeReference\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x17\n\x0fjava_descriptor\x18\x02 \x01(\t\";\n\x11TypeReferenceList\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x1a\n\x12type_reference_ids\x18\x02 \x03(\x05\"\x9f\x01\n\nFileOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12Q\n\x10maven_coordinate\x18\x03 \x01(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12 \n\x18provided_by_build_system\x18\x04 \x01(\x08\"I\n\x14\x43lassFileInJarOrigin\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x16\n\x0e\x66ile_origin_id\x18\x02 \x01(\x05\x12\r\n\x05\x65ntry\x18\x03 \x01(\t\"T\n\x0eTextFileOrigin\x12\x16\n\x0e\x66ile_origin_id\x18\x01 \x01(\x05\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x15\n\rcolumn_number\x18\x03 \x01(\x05\"U\n\x0fMavenCoordinate\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61rtifact_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"\x9a\x01\n\tBuildInfo\x12\x13\n\x0b\x63lass_count\x18\x01 \x01(\x05\x12\x13\n\x0b\x66ield_count\x18\x02 \x01(\x05\x12\x14\n\x0cmethod_count\x18\x03 \x01(\x05\x12\x18\n\x10live_class_count\x18\x04 \x01(\x05\x12\x18\n\x10live_field_count\x18\x05 \x01(\x05\x12\x19\n\x11live_method_count\x18\x06 \x01(\x05\"\xd5\n\n\x14\x42lastRadiusContainer\x12M\n\x11\x66ile_origin_table\x18\x01 \x03(\x0b\x32\x32.com.android.tools.r8.blastradius.proto.FileOrigin\x12\x64\n\x1e\x63lass_file_in_jar_origin_table\x18\x02 \x03(\x0b\x32<.com.android.tools.r8.blastradius.proto.ClassFileInJarOrigin\x12W\n\x16maven_coordinate_table\x18\x03 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MavenCoordinate\x12U\n\x15\x66ield_reference_table\x18\x04 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.FieldReference\x12W\n\x16method_reference_table\x18\x05 \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.MethodReference\x12U\n\x15proto_reference_table\x18\x06 \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.ProtoReference\x12S\n\x14type_reference_table\x18\x07 \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.TypeReference\x12\\\n\x19type_reference_list_table\x18\x08 \x03(\x0b\x32\x39.com.android.tools.r8.blastradius.proto.TypeReferenceList\x12T\n\x15kept_class_info_table\x18\t \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptClassInfo\x12T\n\x15kept_field_info_table\x18\n \x03(\x0b\x32\x35.com.android.tools.r8.blastradius.proto.KeptFieldInfo\x12V\n\x16kept_method_info_table\x18\x0b \x03(\x0b\x32\x36.com.android.tools.r8.blastradius.proto.KeptMethodInfo\x12W\n\x16keep_constraints_table\x18\x0c \x03(\x0b\x32\x37.com.android.tools.r8.blastradius.proto.KeepConstraints\x12\x61\n\x1ckeep_rule_blast_radius_table\x18\r \x03(\x0b\x32;.com.android.tools.r8.blastradius.proto.KeepRuleBlastRadius\x12n\n#global_keep_rule_blast_radius_table\x18\x0e \x03(\x0b\x32\x41.com.android.tools.r8.blastradius.proto.GlobalKeepRuleBlastRadius\x12\x45\n\nbuild_info\x18\x0f \x01(\x0b\x32\x31.com.android.tools.r8.blastradius.proto.BuildInfo*\x1f\n\x0bKeepRuleTag\x12\x10\n\x0cPACKAGE_WIDE\x10\x00*H\n\x0eKeepConstraint\x12\x12\n\x0e\x44ONT_OBFUSCATE\x10\x00\x12\x11\n\rDONT_OPTIMIZE\x10\x01\x12\x0f\n\x0b\x44ONT_SHRINK\x10\x02\x42\x45\n&com.android.tools.r8.blastradius.protoB\x19KeepRuleBlastRadiusProtosP\001\x62\x06proto3') + + _globals = globals() + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) + _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keep_radius_pb2', _globals) + if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.android.tools.r8.blastradius.protoB\031KeepRuleBlastRadiusProtosP\001' + _globals['_KEEPRULETAG']._serialized_start = 3332 + _globals['_KEEPRULETAG']._serialized_end = 3363 + _globals['_KEEPCONSTRAINT']._serialized_start = 3365 + _globals['_KEEPCONSTRAINT']._serialized_end = 3437 + _globals['_KEEPRULEBLASTRADIUS']._serialized_start = 62 + _globals['_KEEPRULEBLASTRADIUS']._serialized_end = 349 + _globals['_BLASTRADIUS']._serialized_start = 351 + _globals['_BLASTRADIUS']._serialized_end = 470 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_start = 472 + _globals['_GLOBALKEEPRULEBLASTRADIUS']._serialized_end = 599 + _globals['_KEEPCONSTRAINTS']._serialized_start = 601 + _globals['_KEEPCONSTRAINTS']._serialized_end = 707 + _globals['_KEPTCLASSINFO']._serialized_start = 709 + _globals['_KEPTCLASSINFO']._serialized_end = 805 + _globals['_KEPTFIELDINFO']._serialized_start = 807 + _globals['_KEPTFIELDINFO']._serialized_end = 903 + _globals['_KEPTMETHODINFO']._serialized_start = 905 + _globals['_KEPTMETHODINFO']._serialized_end = 1003 + _globals['_FIELDREFERENCE']._serialized_start = 1005 + _globals['_FIELDREFERENCE']._serialized_end = 1102 + _globals['_METHODREFERENCE']._serialized_start = 1104 + _globals['_METHODREFERENCE']._serialized_end = 1203 + _globals['_PROTOREFERENCE']._serialized_start = 1205 + _globals['_PROTOREFERENCE']._serialized_end = 1280 + _globals['_TYPEREFERENCE']._serialized_start = 1282 + _globals['_TYPEREFERENCE']._serialized_end = 1334 + _globals['_TYPEREFERENCELIST']._serialized_start = 1336 + _globals['_TYPEREFERENCELIST']._serialized_end = 1395 + _globals['_FILEORIGIN']._serialized_start = 1398 + _globals['_FILEORIGIN']._serialized_end = 1557 + _globals['_CLASSFILEINJARORIGIN']._serialized_start = 1559 + _globals['_CLASSFILEINJARORIGIN']._serialized_end = 1632 + _globals['_TEXTFILEORIGIN']._serialized_start = 1634 + _globals['_TEXTFILEORIGIN']._serialized_end = 1718 + _globals['_MAVENCOORDINATE']._serialized_start = 1720 + _globals['_MAVENCOORDINATE']._serialized_end = 1805 + _globals['_BUILDINFO']._serialized_start = 1808 + _globals['_BUILDINFO']._serialized_end = 1962 + _globals['_BLASTRADIUSCONTAINER']._serialized_start = 1965 + _globals['_BLASTRADIUSCONTAINER']._serialized_end = 3330 + # @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/performance/r8-analyzer/references/REFLECTION-GUIDE.md b/performance/r8-analyzer/references/REFLECTION-GUIDE.md new file mode 100644 index 0000000..8f23679 --- /dev/null +++ b/performance/r8-analyzer/references/REFLECTION-GUIDE.md @@ -0,0 +1,139 @@ +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. + +- **Example Code:** + `kotlin + val taskClass = Class.forName(className) + val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask` + +- **Suggested Keep Rule:** + \`\`\`proguard + + -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)` + +- **Suggested Keep Rule:** + \`\`\`proguard + + # Keep the class itself (R8 usually handles this, but explicit rules ensure stability) + + -keep class com.example.app.MyService { + (); } \`\`\` + +### 3. Annotation-Based Reflection (Methods/Classes) + +**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` + +- **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 \*; +} +\`\`\` + +### 4. Optional Dependencies (Soft Dependencies) + +**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 \*/ } +\`\`\` + +- **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. + +- **Look for:** `getDeclaredField("...")` or `getDeclaredMethod("...")` followed by `isAccessible = true`. +- **Example Code:** + `kotlin + val secretField = instance::class.java.getDeclaredField("secretMessage") + secretField.isAccessible = true` + +- **Suggested Keep Rule:** + \`\`\`proguard + + # Specifically keep the private field/method by name and type + + -keepclassmembers class com.example.LibraryClass { + private java.lang.String secretMessage; + } + \`\`\` + +### 6. Parcelable (Manual Implementation) + +**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 + }` + +- **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. + +- **Look for:** Unnecessary generic Enum keep rules in ProGuard files. +- **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 diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index d17590e..0493cd7 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-07-31' + last-updated: '2026-07-18' keywords: - android - engage @@ -29,7 +29,7 @@ Follow these steps to assist the developer: - Check if the integration is for TV or mobile. If the integration is for TV, read the TV-specific sections in [patterns.md](references/patterns.md) as well. - Use `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory to identify the corresponding Engage entities and the `client` class name. The `client` field in the JSON provides the full class name. For example, `com.google.android.engage.food.service.AppEngageFoodClient`. - **Note:** Initializing the client class requires a `Context` parameter. For example, `AppEngageFoodClient(context)`. - - Always refer to [common.md](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/common) for common entities. + - Always refer to [common.md](references/common.md) for common entities. - Ask which cluster type they want to publish from the supported cluster types for that vertical. - Find the method to call from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory for the specified cluster. Each method will specify the request it expects. - Get the request structure from [requests.md](references/requests.md) and clusters from [clusters.md](references/clusters.md). Then suggest and use sources to fill the fields in the request structure correctly, along with the required entities and clusters. @@ -69,7 +69,7 @@ Follow these steps to assist the developer: - 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](https://developer.android.com/agents/skills/play/engage-sdk-integration/references/common). + - 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 @@ -102,7 +102,7 @@ Follow these steps to assist the developer: - [Listen Vertical](references/android/guide/playcore/engage/listen.md) - [Read Vertical](references/android/guide/playcore/engage/read.md) - [Shopping Vertical](references/android/guide/playcore/engage/shopping.md) - - [Social Vertical](references/android/guide/playcore/engage/social.md) + - [Social Vertical](https://developer.android.com/guide/playcore/engage/social) - [Travel Vertical](references/android/guide/playcore/engage/travel.md) - [Health and Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) - [Other Verticals](references/android/guide/playcore/engage/otherverticals.md) 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 deleted file mode 100644 index b1e8f78..0000000 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md +++ /dev/null @@ -1,702 +0,0 @@ -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. - -## Integration detail - -The following section captures the integration detail. - -### Terminology - -***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. - -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. - -- Post - - - Image in portrait mode and Timestamp, or - - Image in portrait mode + text content and Timestamp -- Profile - - - Avatar, Name or Handle, Additional image -- Interactions - - - Count and label only, or - - Count and visual (icon) - -**SocialPostEntity** contains profile, post and interaction related metadata. - -- Profile - - - Avatar, Name or Handle, additional text, additional image -- Post - - - Text and Timestamp, or - - Rich media (image or rich URL) and Timestamp, or - - Text and rich media (image or rich URL) and Timestamp, or - - Video preview (thumbnail and duration) and Timestamp -- Interactions - - - Count \& label only, or - - Count \& visual (icon) - -### Pre-work - -Minimum API level: 19 - -Add the `com.google.android.engage:engage-core` library to your app: - - dependencies { - // Make sure you also include that repository in your project's build.gradle file. - implementation 'com.google.android.engage:engage-core:1.6.0' - } - -### Summary - -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: - -| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | -|---|---|---|---| -| Recommendation Cluster(s) | At most 7 | At least 1 (`PortraitMediaEntity`, or `SocialPostEntity`) | At most 50 (`PortraitMediaEntity`, or `SocialPostEntity`) | - -### 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: - -1. `PortraitMediaEntity` -2. `SocialPostEntity` - -The charts below outline available attributes and requirements for each type. - -#### `PortraitMediaEntity` - -| Attribute | Requirement | Description | Format | -|---|---|---|---| -| Action URI | **Required** for all surfaces other than Google TV | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | -| PlatformSpecificPlayback | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | -| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | -| Comments Summary | Optional | Summary of comments for the post. | String | -| **Post related metadata (Required)** |||| -| Image(s) | Required | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | -| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | -| Is video content | Optional | Is the post a video? | boolean | -| Video duration | Optional | The duration of the video in milliseconds. | Long | -| **Profile related metadata (Optional)** |||| -| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel" | String(recommended max 25 chars) | -| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Additional Image | Optional | Profile badge. for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Interactions related metadata (Optional)** |||| -| Count | Optional | Indicate the number of interactions, for example - "3.7 M.". **Note:** If both Count and Count Value are provided, Count will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| CountWithOptionalLabel | Optional | Indicate the number of interactions with an optional label, for example - "3.7 M Likes.". **Note:** If both CountWithOptionalLabel and Count Value are provided, one of them will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| Count Value | Optional | The number of interactions as a value. **Note:** Provide Count Value instead of Count if your app doesn't handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count is used. | Long | -| Label | Optional | Indicate what the interaction label is for. For example - "Likes". | String | -| Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| -| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | -| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | - -#### `SocialPostEntity` - -| Attribute | Requirement | Description | Format | -|---|---|---|---| -| Action URI | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | -| PlatformSpecificPlayback URIs | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | -| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | -| Comments Summary | Optional | Summary of comments for the post. | String | -| **Post related metadata (Required)** At least one of TextContent, Image or WebContent is required |||| -| Image(s) | Optional | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | -| **Video Content (Optional)** |||| -| Duration | Required | The duration of the video in milliseconds. | Long | -| Image | Required | Preview image of the video content. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Link Preview (Optional)** |||| -| Link Preview - Title | Required | Text to indicate the title of the web page content | String | -| Link Preview - Hostname | Required | Text to indicate the web page owner, eg "INSIDER" | String | -| Link Preview - Image | Optional | Hero image for the web content | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | -| **Profile related metadata (Optional)** |||| -| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel." | String(recommended max 25 chars) | -| Additional Text | Optional | Could be used as profile id or handle or additional metadata For example "@John-Doe", "5M followers", "You might like", "Trending", "5 new posts" | String(recommended max 40 chars) | -| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| Additional Image | Optional | Profile badge, for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **Interactions related metadata (Optional)** |||| -| Count | Required | Indicate the number of interactions, for example - "3.7 M." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| CountWithOptionalLabel | Required | Indicate the number of interactions with an optional label, for example - "3.7 M Likes." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | -| Label | Optional If not provided, **Visual** must be provided. | Indicate what the interaction is for. For example - "Likes." | String (recommended max 20 chars for count + label combined) | -| Visual | Optional If not provided, **Label** must be provided. | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | -| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| -| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | -| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | - -#### Image specifications - -The images are required to be hosted on public CDNs so that Google can access -them. - -*File formats* - -PNG, JPG, static GIF, WebP - -*Maximum file size* - -5120 KB - -*Additional recommendations* - -- **Image safe area:** Put your important content in the center 80% of the image. -- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. - -### 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) - -`AppEngageSocialClient` is responsible for publishing social clusters. - -There are following APIs to publish clusters in the client: - -- `isServiceAvailable` -- `publishRecommendationClusters` -- `publishUserAccountManagementRequest` -- `updatePublishStatus` -- `deleteRecommendationsClusters` -- `deleteUserManagementCluster` -- `deleteClusters` - -#### `isServiceAvailable` - -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) - -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 | -| Featured Cluster | `TYPE_FEATURED` | 2 | -| Continuation Cluster | `TYPE_CONTINUATION` | 3 | -| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | -| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | - -### Kotlin - - 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 { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - } - -### Java - - ServiceAvailabilityRequest request = - new ServiceAvailabilityRequest.Builder() - .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) - .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) - .build(); - - client.isServiceAvailable(request).addOnCompleteListener(task -> { - if (task.isSuccessful()) { - Map availabilityMap = task.getResult(); - if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { - // Proceed with publishing continuation content - } - if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { - // Proceed with publishing recommendation content - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - }); - -###### 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. - -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) - -### Kotlin - - client.isServiceAvailable.addOnCompleteListener { task -> - if (task.isSuccessful) { - // Handle IPC call success - if(task.result) { - // Service is available on the device, proceed with content publish - // calls. - } else { - // Service is not available, no further action is needed. - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - } - -### Java - - client.isServiceAvailable().addOnCompleteListener(task - > { - if (task.isSuccessful()) { - // Handle success - if(task.getResult()) { - // Service is available on the device, proceed with content publish - // calls. - } else { - // Service is not available, no further action is needed. - } - } else { - // The IPC call itself fails, proceed with error handling logic here, - // such as retry. - } - }); - -> [!NOTE] -> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. - -#### `publishRecommendationClusters` - -This API is used to publish a list `RecommendationCluster` objects. - -A `RecommendationCluster` object can have the following attributes: - -| Attribute | Requirement | Description | -|---|---|---| -| List of SocialPostEntity, or PortraitMediaEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | -| Title | **Required** | The title for the Recommendation Cluster (for example, *Latest from your friends*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | -| Subtitle | Optional | The subtitle for the Recommendation Cluster. | -| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | - -> [!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. - -> [!IMPORTANT] -> **Important:** For social apps, it's critical to update recommendations 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. - -### Kotlin - - client.publishRecommendationClusters( - PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster( - RecommendationCluster.Builder() - .addEntity(entity1) - .addEntity(entity2) - .setTitle("Latest from your friends") - .build()) - .build()) - -### Java - - client.publishRecommendationClusters( - new PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster( - new RecommendationCluster.Builder() - .addEntity(entity1) - .addEntity(entity2) - .setTitle("Latest from your friends") - .build()) - .build()); - -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. - -#### `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) - -The following metadata is part of the Sign In Card - - -| Attribute | Requirement | Description | -|---|---|---| -| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | -| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | -| Title | Optional - If not provided, Image must be provided | Title on the Card | -| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | -| Subtitle | Optional | Optional Subtitle on the Card | - -> [!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. - -### Kotlin - - var SIGN_IN_CARD_ENTITY = - SignInCardEntity.Builder() - .addPosterImage( - Image.Builder() - .setImageUri(Uri.parse("http://www.x.com/image.png")) - .setImageHeightInPixel(500) - .setImageWidthInPixel(500) - .build()) - .setActionText("Sign In") - .setActionUri(Uri.parse("http://xx.com/signin")) - .build() - - client.publishUserAccountManagementRequest( - PublishUserAccountManagementRequest.Builder() - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) - .build()); - -### Java - - SignInCardEntity SIGN_IN_CARD_ENTITY = - new SignInCardEntity.Builder() - .addPosterImage( - new Image.Builder() - .setImageUri(Uri.parse("http://www.x.com/image.png")) - .setImageHeightInPixel(500) - .setImageWidthInPixel(500) - .build()) - .setActionText("Sign In") - .setActionUri(Uri.parse("http://xx.com/signin")) - .build(); - - client.publishUserAccountManagementRequest( - new PublishUserAccountManagementRequest.Builder() - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) - .build()); - -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. - -#### `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 : - -- 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. -- It helps developers provide insights into when the data is published versus not. -- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. - -The list of eligible publish status codes are : - - // Content is published - AppEngagePublishStatusCode.PUBLISHED, - - // Content is not published as user is not signed in - AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, - - // Content is not published as user is not subscribed - AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, - - // Content is not published as user location is ineligible - AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, - - // Content is not published as there is no eligible content - AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, - - // Content is not published as the feature is disabled by the client - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, - - // Content is not published as the feature due to a client error - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, - - // Content is not published as the feature due to a service error - // Available in v1.3.1 - AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, - - // Content is not published due to some other reason - // 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** - -### Kotlin - - client.updatePublishStatus( - PublishStatusRequest.Builder() - .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) - .build()) - -### Java - - client.updatePublishStatus( - new PublishStatusRequest.Builder() - .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) - .build()); - -#### `deleteRecommendationClusters` - -This API is used to delete the content of Recommendation Clusters. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteRecommendationClusters() - -### Java - - 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. - -#### `deleteUserManagementCluster` - -This API is used to delete the content of UserAccountManagement Cluster. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteUserManagementCluster() - -### Java - - 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. - -#### `deleteClusters` - -This API is used to delete the content of a given cluster type. - -> [!IMPORTANT] -> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. - -### Kotlin - - client.deleteClusters( - DeleteClustersRequest.Builder() - .addClusterType(ClusterType.TYPE_RECOMMENDATION) - ... - .build()) - -### Java - - client.deleteClusters( - new DeleteClustersRequest.Builder() - .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. - -#### 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. - - client.publishRecommendationClusters( - new PublishRecommendationClustersRequest.Builder() - .addRecommendationCluster(...) - .build()) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - // do something - } else { - Exception exception = task.getException(); - if (exception instanceof AppEngageException) { - @AppEngageErrorCode - int errorCode = ((AppEngageException) exception).getErrorCode(); - if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { - // do something - } - } - } - }); - -The error is returned as an `AppEngageException` with the cause included as an -error code. - -| Error code | Error name | Note | -|---|---|---| -| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | -| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | -| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | -| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | -| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | -| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | -| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | - -### 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. - -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. - -### Kotlin - - class AppEngageBroadcastReceiver : BroadcastReceiver(){ - // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION - // broadcast is received - } - - fun registerBroadcastReceivers(context: Context){ - var context = context - context = context.applicationContext - - // Register Recommendation Cluster Publish Intent - context.registerReceiver(AppEngageBroadcastReceiver(), - IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), - com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, - /*scheduler=*/null) - } - -### Java - - class AppEngageBroadcastReceiver extends BroadcastReceiver { - // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast - // is received - } - - public static void registerBroadcastReceivers(Context context) { - - context = context.getApplicationContext(); - - // Register Recommendation Cluster Publish Intent - context.registerReceiver(new AppEngageBroadcastReceiver(), - new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), - com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, - /*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. - - - - - - - - - -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). - -## 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. - -## Next steps - -After completing this integration, your next steps are as follows: - -- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. -- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. -- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. -- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation**, clusters will be published and visible to users. \ 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 c0c0058..49d8975 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 @@ -48,18 +48,12 @@ Before you begin, complete the following steps: 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 in + the `AndroidManifest.xml` file. - - - - - + > [!IMPORTANT] > **Important:** The Engage service environment declaration is only needed for your release build. For your local testing with the verification app, don't include this element. diff --git a/play/engage-sdk-integration/references/common.md b/play/engage-sdk-integration/references/common.md new file mode 100644 index 0000000..52c3d5e --- /dev/null +++ b/play/engage-sdk-integration/references/common.md @@ -0,0 +1,519 @@ +This file defines common data models and entities used across the Engage SDK. + + { + "entities": { + "Image": { + "package": "com.google.android.engage.common.datamodel.Image", + "fields": { + "imageUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setImageUri(Uri)", + "getter": "getImageUri()" + }, + "height": { + "type": "Integer", + "requirement": "Optional" + }, + "width": { + "type": "Integer", + "requirement": "Optional" + }, + "accessibilityText": { + "type": "String", + "requirement": "Required", + "setter": "setAccessibilityText(String)", + "getter": "getAccessibilityText()" + }, + "theme": { + "type": "ImageThemeEnum", + "requirement": "Optional" + }, + "cropType": { + "type": "ImageCropTypeEnum", + "requirement": "Optional" + }, + "imageHeightInPixel": { + "requirement": "Required", + "setter": "setImageHeightInPixel(int)", + "type": "Integer", + "getter": "getImageHeightInPixel()" + }, + "imageWidthInPixel": { + "requirement": "Required", + "setter": "setImageWidthInPixel(int)", + "type": "Integer", + "getter": "getImageWidthInPixel()" + }, + "imageTheme": { + "requirement": "Optional", + "setter": "setImageTheme(@ImageTheme int)", + "type": "@ImageTheme int", + "getter": "getImageTheme()" + }, + "imageCropType": { + "requirement": "Optional", + "setter": "setImageCropType(@ImageCropType int)", + "type": "@ImageCropType int", + "getter": "getImageCropType()" + } + } + }, + "Price": { + "package": "com.google.android.engage.common.datamodel.Price", + "fields": { + "currentPrice": { + "type": "String", + "requirement": "Required", + "setter": "setCurrentPrice(String)", + "getter": "getCurrentPrice()" + }, + "strikethroughPrice": { + "type": "String", + "requirement": "Optional", + "setter": "setStrikethroughPrice(String)", + "getter": "getStrikethroughPrice()" + } + } + }, + "Rating": { + "package": "com.google.android.engage.common.datamodel.Rating", + "fields": { + "ratingValue": { + "type": "Float", + "requirement": "Required" + }, + "maxValue": { + "type": "Double", + "requirement": "Required", + "setter": "setMaxValue(double)", + "getter": "getMaxValue()" + }, + "ratingCount": { + "type": "Integer", + "requirement": "Optional" + }, + "ratingCountValue": { + "type": "String", + "requirement": "Optional" + }, + "currentValue": { + "requirement": "Required", + "setter": "setCurrentValue(double)", + "type": "Double", + "getter": "getCurrentValue()" + }, + "count": { + "requirement": "Optional", + "setter": "setCount(String)", + "type": "String", + "getter": "getCount()" + }, + "countValue": { + "requirement": "Optional", + "setter": "setCountValue(long)", + "type": "Long", + "getter": "getCountValue()" + } + } + }, + "DisplayTimeWindow": { + "package": "com.google.android.engage.common.datamodel.DisplayTimeWindow", + "fields": { + "startTimeMillis": { + "type": "Long", + "requirement": "Required" + }, + "endTimeMillis": { + "type": "Long", + "requirement": "Required" + }, + "startTimestampMillis": { + "requirement": "Optional", + "setter": "setStartTimestampMillis(long)", + "type": "Long", + "getter": "getStartTimestampMillis()" + }, + "endTimestampMillis": { + "requirement": "Optional", + "setter": "setEndTimestampMillis(long)", + "type": "Long", + "getter": "getEndTimestampMillis()" + } + } + }, + "AccountProfile": { + "package": "com.google.android.engage.common.datamodel.AccountProfile", + "fields": { + "accountId": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setAccountId(@NonNull String)", + "getter": "getAccountId()" + }, + "profileId": { + "type": "@NonNull String", + "requirement": "Optional", + "setter": "setProfileId(@NonNull String)", + "getter": "getProfileId()" + }, + "accountName": { + "type": "String", + "requirement": "Optional" + }, + "profileImage": { + "type": "Image", + "requirement": "Optional" + }, + "locale": { + "requirement": "Optional", + "setter": "setLocale(@NonNull String)", + "type": "@NonNull String", + "getter": "getLocale()" + } + } + }, + "Address": { + "package": "com.google.android.engage.common.datamodel.Address", + "fields": { + "city": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setCity(@NonNull String)", + "getter": "getCity()" + }, + "country": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setCountry(@NonNull String)", + "getter": "getCountry()" + }, + "displayAddress": { + "type": "@NonNull String", + "requirement": "Required", + "setter": "setDisplayAddress(@NonNull String)", + "getter": "getDisplayAddress()" + }, + "streetAddress": { + "type": "String", + "requirement": "Optional", + "setter": "setStreetAddress(String)", + "getter": "getStreetAddress()" + }, + "state": { + "type": "String", + "requirement": "Optional", + "setter": "setState(String)", + "getter": "getState()" + }, + "zipCode": { + "type": "String", + "requirement": "Optional", + "setter": "setZipCode(String)", + "getter": "getZipCode()" + }, + "neighborhood": { + "type": "String", + "requirement": "Optional", + "setter": "setNeighborhood(String)", + "getter": "getNeighborhood()" + } + } + }, + "Badge": { + "package": "com.google.android.engage.common.datamodel.Badge", + "fields": { + "text": { + "type": "String", + "requirement": "Optional", + "setter": "setText(String)", + "getter": "getText()" + }, + "image": { + "type": "Image", + "requirement": "Optional", + "setter": "setImage(Image)", + "getter": "getImage()" + } + } + }, + "LocalizedTimestamp": { + "package": "com.google.android.engage.common.datamodel.LocalizedTimestamp", + "fields": { + "timestamp": { + "type": "Instant", + "requirement": "Required", + "setter": "setTimestamp(Instant)", + "getter": "getTimestamp()" + }, + "timezone": { + "type": "DateTimeZone", + "requirement": "Required", + "setter": "setTimezone(DateTimeZone)", + "getter": "getTimezone()" + } + } + }, + "SignInCardEntity": { + "package": "com.google.android.engage.common.datamodel.SignInCardEntity", + "fields": { + "actionText": { + "type": "String", + "requirement": "Required", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "subtitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSubtitle(String)", + "getter": "getSubtitle()" + }, + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + } + } + }, + "GenericFeaturedEntity": { + "package": "com.google.android.engage.common.datamodel.GenericFeaturedEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + }, + "SubscriptionEntity": { + "package": "com.google.android.engage.common.datamodel.SubscriptionEntity", + "fields": {} + }, + "UserSettingsCardEntity": { + "package": "com.google.android.engage.common.datamodel.UserSettingsCardEntity", + "fields": { + "actionText": { + "type": "String", + "requirement": "Required", + "setter": "setActionText(String)", + "getter": "getActionText()" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "subtitle": { + "type": "String", + "requirement": "Optional", + "setter": "setSubtitle(String)", + "getter": "getSubtitle()" + }, + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + } + } + }, + "ArticleEntity": { + "package": "com.google.android.engage.common.datamodel.ArticleEntity", + "fields": { + "entityId": { + "type": "String", + "requirement": "Optional", + "setter": "setEntityId(String)", + "getter": "getEntityId()" + }, + "posterImages": { + "type": "List", + "requirement": "Required", + "adder": "addPosterImage(Image)", + "getter": "getPosterImages()", + "adderAll": "addPosterImages(List)" + }, + "actionUri": { + "type": "Uri", + "requirement": "Required", + "setter": "setActionUri(Uri)", + "getter": "getActionUri()" + }, + "title": { + "type": "String", + "requirement": "Optional", + "setter": "setTitle(String)", + "getter": "getTitle()" + }, + "description": { + "type": "String", + "requirement": "Optional", + "setter": "setDescription(String)", + "getter": "getDescription()" + }, + "subtitles": { + "type": "List", + "requirement": "Optional", + "adder": "addSubtitle(String)", + "adderAll": "addSubtitles(List)" + }, + "badges": { + "type": "List", + "requirement": "Optional", + "adder": "addBadge(Badge)", + "adderAll": "addBadges(List)" + }, + "contentCategories": { + "type": "List<@EligibleContentCategory int>", + "requirement": "Optional", + "adder": "addContentCategory(@EligibleContentCategory int)", + "adderAll": "addContentCategories(List)" + }, + "progressPercentage": { + "type": "Integer", + "requirement": "Required", + "setter": "setProgressPercentage(int)", + "getter": "getProgressPercentage()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "lastEngagementTimestampMillis": { + "type": "Long", + "requirement": "Required", + "setter": "setLastEngagementTimestampMillis(long)", + "getter": "getLastEngagementTimestampMillis()", + "requiredFor": [ + "ContinuationCluster" + ] + }, + "source": { + "type": "Badge", + "requirement": "Optional", + "setter": "setSource(Badge)", + "getter": "getSource()" + }, + "lastContentPublishTimestampMillis": { + "type": "Long", + "requirement": "Optional", + "setter": "setLastContentPublishTimestampMillis(Long)", + "getter": "getLastContentPublishTimestampMillis()" + }, + "displayTimeWindows": { + "type": "List", + "requirement": "Optional", + "adder": "addDisplayTimeWindow(DisplayTimeWindow)", + "getter": "getDisplayTimeWindows()" + }, + "allDisplayTimeWindows": { + "type": "List>", + "requirement": "Optional", + "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", + "adderAll": "addAllDisplayTimeWindow(List)" + } + } + } + }, + "intents": { + "ACTION_PUBLISH_RECOMMENDATION": "com.google.android.engage.action.PUBLISH_RECOMMENDATION", + "ACTION_PUBLISH_FEATURED": "com.google.android.engage.action.PUBLISH_FEATURED", + "ACTION_PUBLISH_CONTINUATION": "com.google.android.engage.action.PUBLISH_CONTINUATION" + }, + "imports": [ + "com.google.android.engage.service.AppEngagePublishClient", + "com.google.android.engage.service.AppEngageErrorCode", + "com.google.android.engage.service.AppEngageException", + "com.google.android.engage.service.AppEngagePublishStatusCode" + ] + } \ 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 new file mode 100644 index 0000000..6784606 --- /dev/null +++ b/play/play-billing-library-version-upgrade/SKILL.md @@ -0,0 +1,93 @@ +--- +name: play-billing-library-version-upgrade +description: Use this skill when upgrading or migrating an Android project from any + legacy Google Play Billing Library (PBL) version to the latest stable version of + PBL. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-07-02' + keywords: + - android + - play billing + - play billing library + - pbl + - upgrade + - migration + - deprecation + - google play +--- + +## Phase 0: Intent Message + +**Reporting Action**: Before proceeding, immediately tell the user: "I will +upgrade Play Billing Library to the latest version." + +## Phase 1: Discovery \& Situational Awareness + +1. **Primary Check (Build Version)** : Locate the project's billing dependency (e.g., `com.android.billingclient:billing`) in `build.gradle`, `build.gradle.kts`, or `libs.versions.toml`. +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. + - **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. +- **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: + +- **[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). +- **Developer Documentation**: Consult your knowledge of the Google Play Billing documentation regarding the relevant features used in this app (e.g., Subscriptions, One-Time Products). +- **Develop the Plan**: Identify every specific code change required (API removals, class replacements, logic shifts) and print this out as a checklist. + +## 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.* + +### Step 1: SDK \& Environment Alignment + +- **Action** : Update `build.gradle` to meet SDK requirements (e.g., "PBL 9 requires `compileSdk` 35"). +- **Gradle Version**: Verify if the new library requires a newer Android Gradle Plugin (AGP) or Kotlin version. + +### Step 2: Intent-based Refactoring + +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. + +### Step 3: Sequential Verification (Only applicable for Stepped Migrations) + +1. **Upgrade** to the first major intermediate version in your path. +2. **Run `./gradlew assembleDebug`** to verify no intermediate breaking changes were missed. +3. **Repeat** until you reach the final target version. + +### Step 4: Final Validation Checklist + +1. **Smart Checklist Verification**: +2. Open [references/version-checklist.md](references/version-checklist.md) and locate the **Smart Version-Specific Checklist**. +3. **Action**: For every version between your \[Detected Effective Version\] and \[Detected New Version\], verify that every item has been addressed in the code using "Find in Files" or structural analysis. +4. **Tests** : Run all unit and implementation tests (`./gradlew test`). +5. **Clean Build** : Verify the project completes a full clean build: `./gradlew clean assembleDebug`. Then, run `./gradlew sync` and `./gradlew build` so that the user can immediately test the new version manually. + +## Final Report + +Explain the "Why" to the developer: + +- "I updated your SDK to \[Version\] because PBL \[Version\] requires it for \[Reason from docs\]." +- "I removed your custom `retryConnection()` logic because it is now handled natively by the library using `enableAutoServiceReconnection()`." +- "Successfully upgraded from PBL \[Old\] to PBL \[New\] and verified with unit tests. Based on an analysis of features in the latest library and this application's current feature set, I suggest exploring \[New Feature\] (e.g., Prepaid Plans or Installments) from the latest release because it is now available but not yet implemented." 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 new file mode 100644 index 0000000..462f373 --- /dev/null +++ b/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md @@ -0,0 +1,1226 @@ +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. + +## Google Play Billing Library 9.1.0 Release (2026-06-18) + +Version 9.1.0 of the Google Play Billing Library and Kotlin extensions are now available. + +### Summary of changes + +- New APIs for [Billing Choice](https://developer.android.com/google/play/billing/billingchoice): + - Added [`BillingClient.getBillingChoiceInfoAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getBillingChoiceInfoAsync(com.android.billingclient.api.GetBillingChoiceInfoParams,com.android.billingclient.api.BillingChoiceInfoResponseListener)) to retrieve information about billing choices available to the user. + - Added [`BillingChoiceInfo`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfo) class which contains the response details, including image URLs and loyalty information. + - Added [`GetBillingChoiceInfoParams`](https://developer.android.com/reference/com/android/billingclient/api/GetBillingChoiceInfoParams) to configure the request. + - Added [`BillingChoiceInfoResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingChoiceInfoResponseListener) to receive the callback. + - Added Kotlin extension [`BillingClient.getBillingChoiceInfo()`](https://developer.android.com/reference/kotlin/com/android/billingclient/api/package-summary#(com.android.billingclient.api.BillingClient).getBillingChoiceInfo(com.android.billingclient.api.GetBillingChoiceInfoParams)) suspend function. + - Added [`BillingClient.showBillingProgramInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showBillingProgramInformationDialog(android.app.Activity,com.android.billingclient.api.BillingProgramInformationDialogParams,com.android.billingclient.api.BillingProgramInformationDialogListener)) to show an information dialog for a billing program. + - Added [`BillingProgramInformationDialogParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogParams) to configure the dialog. + - Added [`BillingProgramInformationDialogListener`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramInformationDialogListener) to receive the callback. + - Added [`BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails) to provide details about billing choice availability. + - Added [`ChoiceScreenType`](https://developer.android.com/reference/com/android/billingclient/api/BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType) to specify the type of choice screen. + +## 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. + +### 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. + + > [!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. + + 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. + +## Google Play Billing Library 8.3.0 Release (2025-12-23) + +Version 8.3.0 of the Google Play Billing Library and Kotlin extensions are now available. + +### Summary of changes + +- New APIs for [external payments](https://developer.android.com/google/play/billing/externalpaymentlinks): + + - Added classes to support external payments flow: + - [`BillingProgram.EXTERNAL_PAYMENTS`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingProgram#EXTERNAL_PAYMENTS) + - [`EnableBillingProgramParams`](https://developer.android.com/reference/com/android/billingclient/api/EnableBillingProgramParams) + - [`DeveloperBillingOptionParams`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperBillingOptionParams) + - [`DeveloperProvidedBillingDetails`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails) + - [`DeveloperProvidedBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingListener) + - Added [`enableBillingProgram(EnableBillingProgramParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableBillingProgram(com.android.billingclient.api.EnableBillingProgramParams)) to enable external payments. + - Added [`BillingFlowParams.Builder.enableDeveloperBillingOption`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#enableDeveloperBillingOption(com.android.billingclient.api.DeveloperBillingOptionParams)) to launch the external payments flow. + +## 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. + +### Bug fixes + +- Fixed a bug in [`isBillingProgramAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isBillingProgramAvailableAsync(int,com.android.billingclient.api.BillingProgramAvailabilityListener)) and [`createBillingProgramReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createBillingProgramReportingDetailsAsync(com.android.billingclient.api.BillingProgramReportingDetailsParams,com.android.billingclient.api.BillingProgramReportingDetailsListener)). Update to version 8.2.1 to use these APIs introduced in 8.2.0. + +## Google Play Billing Library 8.2.0 Release (2025-12-09) + +Version 8.2.0 of the Google Play Billing Library and Kotlin extensions are now available. + +### Summary of changes + +- New APIs for [external content links](https://developer.android.com/google/play/billing/externalcontentlinks) and [external offers](https://developer.android.com/google/play/billing/external): + + - Added [`enableBillingProgram`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableBillingProgram(int)) to setup the `BillingClient` for these programs. + - Added [`isBillingProgramAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isBillingProgramAvailableAsync(int,com.android.billingclient.api.BillingProgramAvailabilityListener)) to determine user eligibility. + - Added [`createBillingProgramReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createBillingProgramReportingDetailsAsync(com.android.billingclient.api.BillingProgramReportingDetailsParams,com.android.billingclient.api.BillingProgramReportingDetailsListener)) to create the external transaction token that must be used for reporting. + - Added [`launchExternalLink`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchExternalLink(android.app.Activity,com.android.billingclient.api.LaunchExternalLinkParams,com.android.billingclient.api.LaunchExternalLinkResponseListener)) to initiate the external link to a digital content offer or an app download. +- Changes to the [external offers program](https://developer.android.com/google/play/billing/external): + + - There are policy changes for the external offers program. See [program changes](https://support.google.com/googleplay/android-developer/answer/16505463) for details. To understand how to launch external offer flows with the new APIs, see the [integration guide](https://developer.android.com/google/play/billing/external/integration). + - Deprecated the [`BillingClient.Builder.enableExternalOffer`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableExternalOffer()) API. + - Deprecated the [`isExternalOfferAvailableAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isExternalOfferAvailableAsync(com.android.billingclient.api.ExternalOfferAvailabilityListener)) API. + - Deprecated the [`createExternalOfferReportingDetailsAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createExternalOfferReportingDetailsAsync(com.android.billingclient.api.ExternalOfferReportingDetailsListener)) API. + - Deprecated the [`showExternalOfferInformationDialog`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showExternalOfferInformationDialog(android.app.Activity,com.android.billingclient.api.ExternalOfferInformationDialogListener)) API. + +## Google Play Billing Library 8.1.0 Release (2025-11-06) + +Version 8.1.0 of the Google Play Billing Library and Kotlin extensions are now available. + +### Summary of changes + +- 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. + + 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 `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. + +- 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. +- 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. + +### Summary of changes + +- *In-app items* will now be referred to as *one-time products*. + +- 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. +- 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). +- 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: + - `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. +- 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. +- 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())`. +- 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)). + +## 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. + +### 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). + +## 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. + +### 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). + +## 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. + +### 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. + + - 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). + - 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). + + - 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)). + + - 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)). + + - 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: + + - 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`. + +- 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. + +### Summary of changes + +- Fixed a bug in [`BillingClient.showAlternativeBillingOnlyInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showAlternativeBillingOnlyInformationDialog(android.app.Activity,%20com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener)) where the [`AlternativeBillingOnlyInformationDialogListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingOnlyInformationDialogListener) may not be called in certain cases when the dialog completes. + +## 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. + +### Summary of changes + +- Added APIs to support [external offers](https://developer.android.com/google/play/billing/external) + - Added [`BillingClient.Builder.enableExternalOffer()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableExternalOffer()) to enable the ability to provide external offers. + - Added [`BillingClient.isExternalOfferAvailableAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isExternalOfferAvailableAsync(com.android.billingclient.api.ExternalOfferAvailabilityListener)) to check the availability of providing external offers functionality. + - Added [`BillingClient.showExternalOfferInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showExternalOfferInformationDialog(android.app.Activity,%20com.android.billingclient.api.ExternalOfferInformationDialogListener)) to show an information dialog to users before leading users outside the app. + - Added [`BillingClient.createExternalOfferReportingDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createExternalOfferReportingDetailsAsync(com.android.billingclient.api.ExternalOfferReportingDetailsListener)) to create a payload required to report transactions made through external offers. + +## 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. + +### Summary of changes + +- Added APIs to support [alternative billing only (i.e. without user choice)](https://developer.android.com/google/play/billing/alternative) + - Added [`BillingClient.Builder.enableAlternativeBillingOnly()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBillingOnly()) to functionally enable the ability to offer alternative billing only. + - Added [`BillingClient.isAlternativeBillingOnlyAvailableAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#isAlternativeBillingOnlyAvailableAsync(com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener)) to check the availability of offering alternative billing only. + - Added [`BillingClient.showAlternativeBillingOnlyInformationDialog()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showAlternativeBillingOnlyInformationDialog(android.app.Activity,%20com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener)) to show an information dialog to inform users when alternative billing only is being used. + - Added [`BillingClient.createAlternativeBillingOnlyReportingDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#createAlternativeBillingOnlyReportingDetailsAsync(com.android.billingclient.api.AlternativeBillingOnlyReportingDetailsListener)) to create a payload required to report transactions made through alternative billing only. +- Updated the user choice billing APIs + - Added [`UserChoiceBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceBillingListener) to replace [AlternativeBillingListener](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener) which has been marked as deprecated. + - Added [`UserChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails) to replace [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails) which has been marked as deprecated. + - Added [`BillingClient.Builder.enableUserChoiceBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableUserChoiceBilling(com.android.billingclient.api.UserChoiceBillingListener)) to replace [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)) which has been marked as deprecated. +- Added [`BillingClient.getBillingConfigAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getBillingConfigAsync(com.android.billingclient.api.GetBillingConfigParams,%20com.android.billingclient.api.BillingConfigResponseListener)) to retrieve Google Play country. + +## 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. + +### Summary of changes + +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. + +### 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). + + 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. +- 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). +- 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). + + 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). + + 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. + +## 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. + +### Summary of changes + +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. + +### Summary of changes + +- Added classes to support alternative billing flows on mobile/tablet for users in South Korea: + - [`AlternativeBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener) + - [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails) + - [`AlternativeChoiceDetails.Product`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails.Product) +- Added [`BillingFlowParams.SubscriptionUpdateParams.Builder.setOriginalExternalTransactionId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOriginalExternalTransactionId(java.lang.String)) method to specify the external transaction id of the originating subscription. +- Added [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)) method to allow users in South Korea to select an alternative billing option. + +## 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. + +This version contains the following changes. + +### Summary of changes + +- Added [`ProductDetails.SubscriptionOfferDetails.getOfferId()`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails#getOfferId()) method to retrieve the offer ID. +- Added [`ProductDetails.SubscriptionOfferDetails.getBasePlanId()`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails#getBasePlanId()) method to retrieve the base plan ID. +- Updated the `targetSdkVersion` to 31. + +## 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. + +This version contains the following changes. + +### Summary of 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). +- 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). +- 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. + +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). + +## 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. + +### 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 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. + +- 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 + [`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. + +- 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. + +### Java and Kotlin Bug fixes + +- Fix memory leak when [`endConnection()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#endConnection()) is called. +- Fix issue when the Google Play Billing Library is used by apps which use the single task launch mode. A [`onPurchasesUpdated()`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener#onpurchasesupdated) callback will be triggered when an app is resumed from the Android launcher and the billing dialog was visible prior to being suspended. + +### Unity Bug fixes + +- Update to Java version 3.0.3 to fix memory leak and resolve issue preventing purchases when an app is resumed from the Android launcher and the billing dialog was visible prior to being suspended. + +## 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. + +### Bug fixes + +- Fixed a bug in the Kotlin extension where the coroutine fails with error "Already resumed". +- Fixed unresolved references when the Kotlin extension is used with the kotlinx.coroutines library version 1.4+. + +## 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. + +### Bug fixes + +- Fixed a bug where if the app was killed and restored during the billing flow, `PurchasesUpdatedListener` may not be called with the purchase result. + +## 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. + +### Summary of changes + +- Removed rewarded SKU support. +- Removed the `ChildDirected` and `UnderAgeOfConsent` parameters. +- Removed deprecated developer payload methods. +- Removed deprecated methods `BillingFlowParams.setAccountId()` and `BillingFlowParams.setDeveloperId()`. +- Removed deprecated methods `BillingFlowParams.setOldSkus(String oldSku)` and `BillingFlowParams.addOldSku(String oldSku)`. +- Added nullability annotations. + +### Bug fixes + +- [`SkuDetails.getIntroductoryPriceCycles()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getIntroductoryPriceCycles()) now returns `int` instead of `String`. +- Fixed a bug where the billing flow would be treated as having extra params even if no extra params were set. + +## Google Play Billing Library 2.2.1 Release (2020-05-20) + +Version 2.2.1 of the Google Play Billing library is now available. + +### Bug fixes + +- Updated the default version of the Java Play Billing library that the Kotlin extension depends on. + +## 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). + +### 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. + +To learn more, see [Use Google Play Billing with +Unity](https://developer.android.com/google/play/billing/unity). + +### Summary of changes + +- Java Google Play Billing Library + - 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 [`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). + +## 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). + +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. + +## Google Play Billing Library 2.0.3 Release (2019-08-05) + +Version 2.0.3 of the Google Play Billing library is now available. + +### Bug fixes + +- Fixed a bug where `querySkuDetailsAsync()` would occasionally fail with code `DEVELOPER_ERROR` instead of returning a successful result. + +## 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. + +## 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. + +### Bug fixes + +- Fixed a bug where debug messages were being returned as `null` in some cases. +- Fixed a potential memory leak issue. + +## 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. + +### Purchases must be acknowledged within three days + +> [!NOTE] +> **Note:** This section describes a new requirement for acknowledging all purchases. If you are already consuming purchases using `consumeAsync()`, you don't need to make any further changes if you consume purchases within 3 days, as `consumeAsync()` automatically acknowledges a purchase. Failure to properly acknowledge purchases will result in purchases being refunded. + +> [!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. + +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. + +You can acknowledge a purchase by using one of the following methods: + +- For consumable products, use `consumeAsync()`, found in the client API. +- For products that aren't consumed, use `acknowledgePurchase()`, found in the client API. +- A new `acknowledge()` method is also available in the Server API. + +### 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). + +> [!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). + +### 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. + +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. + +### Consistent offers + +> [!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. + +[`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. +- [`getOriginalPrice()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getOriginalPrice()) + - returns the original price with additional currency formatting. + +### 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. + +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: + +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. + +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`. + +> [!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. + +#### 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. + +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. + +#### 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. + +### Bug fixes + +- [`SkuDetails.getIntroductoryPriceAmountMicros()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getIntroductoryPriceAmountMicros()) now returns a `long` instead of a `String`. + +## 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. + +### Bug fixes + +- Fixed a threading issue introduced in v1.2.1. Background calls no longer block the main thread. + +### Other changes + +- Although using the main thread is still recommended, you can now instantiate the Google Play Billing Library from a background thread. +- Instantiation has been fully migrated to the background thread to reduce the chance of causing ANRs. + +## 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. + +### 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). + +### Other changes + +- Added public constructors for `PurchasesResult` and `SkuDetailsResult` to make testing easier. +- `SkuDetails` objects can use a new method, `getOriginalJson()`. +- All AIDL service calls are now handled by background threads. + +### Bug fixes + +- Null callback listeners are no longer passed into public APIs. + +## 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. + +### 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). +- 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()`. +- Minor bug fixes and code optimizations. + +#### 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. + +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 + +```kotlin +val priceChangeFlowParams = PriceChangeFlowParams.newBuilder() + .setSkuDetails(skuDetailsOfThePriceChangedSubscription) + .build() + +billingClient.launchPriceChangeConfirmationFlow(activity, + priceChangeFlowParams, + object : PriceChangeConfirmationListener() { + override fun onPriceChangeConfirmationResult(responseCode: Int) { + // Handle the result. + } + }) +``` + +### Java + +```java +PriceChangeFlowParams priceChangeFlowParams = + PriceChangeFlowParams.newBuilder() + .setSkuDetails(skuDetailsOfThePriceChangedSubscription) + .build(); + +billingClient.launchPriceChangeConfirmationFlow(activity, + priceChangeFlowParams, + new PriceChangeConfirmationListener() { + @Override + public void onPriceChangeConfirmationResult(int responseCode) { + // Handle the result. + } + }); +``` + +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). + +#### 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. + +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. + +### Kotlin + +```kotlin +private lateinit var mBillingClient: BillingClient +private val mSkuDetailsMap = HashMap() + +private fun querySkuDetails() { + val skuDetailsParamsBuilder = SkuDetailsParams.newBuilder() + mBillingClient.querySkuDetailsAsync(skuDetailsParamsBuilder.build() + ) { responseCode, skuDetailsList -> + if (responseCode == 0) { + for (skuDetails in skuDetailsList) { + mSkuDetailsMap[skuDetails.sku] = skuDetails + } + } + } +} + +private fun startPurchase(skuId: String) { + val billingFlowParams = BillingFlowParams.newBuilder() + .setSkuDetails(mSkuDetailsMap[skuId]) + .build() +} +``` + +### Java + +```java +private BillingClient mBillingClient; +private Map mSkuDetailsMap = new HashMap<>(); + +private void querySkuDetails() { + SkuDetailsParams.Builder skuDetailsParamsBuilder + = SkuDetailsParams.newBuilder(); + mBillingClient.querySkuDetailsAsync(skuDetailsParamsBuilder.build(), + new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse(int responseCode, + List skuDetailsList) { + if (responseCode == 0) { + for (SkuDetails skuDetails : skuDetailsList) { + mSkuDetailsMap.put(skuDetails.getSku(), skuDetails); + } + } + } + }); +} + +private void startPurchase(String skuId) { + BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() + .setSkuDetails(mSkuDetailsMap.get(skuId)) + .build(); +} +``` + +## 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. + +### Summary of changes + +- Added support to specify a proration mode in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) when upgrading/downgrading an existing subscription. +- The `replaceSkusProration` boolean flag in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) is no longer supported. Use `replaceSkusProrationMode` instead. +- [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) now triggers a callback for failed responses. + +### 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. + +### Kotlin + +```kotlin +BillingFlowParams.newBuilder() + .setSku(skuId) + .setType(billingType) + .setOldSku(oldSku) + .setReplaceSkusProrationMode(replaceSkusProrationMode) + .build() +``` + +
+ +
+ +### Java + +```java +BillingFlowParams.newBuilder() + .setSku(skuId) + .setType(billingType) + .setOldSku(oldSku) + .setReplaceSkusProrationMode(replaceSkusProrationMode) + .build(); +``` + +
+ +Google Play supports following proration modes: + +|---|---| +| `IMMEDIATE_WITH_TIME_PRORATION` | Replacement takes effect immediately, and the new expiration time will be prorated and credited or charged to the user. This is the current default behavior. | +| `IMMEDIATE_AND_CHARGE_PRORATED_PRICE` | Replacement takes effect immediately, and the billing cycle remains the same. The price for the remaining period will be charged. **Note**: This option is only available for subscription upgrade. | +| `IMMEDIATE_WITHOUT_PRORATION` | Replacement takes effect immediately, and the new price will be charged on next recurrence time. The billing cycle stays the same. | + +### `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. + +### [`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. + +### Bug fixes + +- Properly exits early in async methods when service is disconnected. +- [`Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder) param objects no longer mutates built objects. +- Issue [68087141](https://issuetracker.google.com/issues/68087141): [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) now trigger callback for failed responses. + +## 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. + +### Important changes + +- Embedded billing permission inside library's manifest. It's not necessary to add the `com.android.vending.BILLING` permission inside Android manifest anymore. +- New builder added to [`BillingClient.Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html) class. +- Introduced builder pattern for [`SkuDetailsParams`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetailsParams.html) class to be used on methods to query SKUs. +- Several API methods were updated for consistency (the same return argument names and order). + +### 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: + +### Kotlin + +```kotlin +billingClient = BillingClient.newBuilder(context).setListener(this).build() +``` + +### Java + +```java +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: + +### Kotlin + +```kotlin +BillingFlowParams.newBuilder().setSku(skuId) + .setType(billingType) + .setOldSku(oldSku) + .build() + +// Then, use the BillingFlowParams to start the purchase flow +val responseCode = billingClient.launchBillingFlow(builder.build()) +``` + +### Java + +```java +BillingFlowParams.newBuilder().setSku(skuId) + .setType(billingType) + .setOldSku(oldSku) + .build(); + +// Then, use the BillingFlowParams to start the purchase flow +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: + +### Kotlin + +```kotlin +val params = SkuDetailsParams.newBuilder() +params.setSkusList(skuList) + .setType(itemType) +billingClient.querySkuDetailsAsync(params.build(), object : SkuDetailsResponseListener() { + ... +}) +``` + +### Java + +```java +SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); +params.setSkusList(skuList) + .setType(itemType); +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: + +### Kotlin + +```kotlin +fun onSkuDetailsResponse(@BillingResponse responseCode: Int, skuDetailsList: List) +``` + +### Java + +```java +public void onSkuDetailsResponse(@BillingResponse int responseCode, List skuDetailsList) +``` + +#### Parameters order changed on `onConsumeResponse()` method + +The order of arguments for +[`onConsumeResponse`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeResponseListener.html#onConsumeResponse(int,%0Ajava.lang.String)) from the +[`ConsumeResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeResponseListener.html) +interface has changed to be consistent across our API: + +### Kotlin + +```kotlin +fun onConsumeResponse(@BillingResponse responseCode: Int, outToken: String) +``` + +### Java + +```java +public void onConsumeResponse(@BillingResponse int responseCode, String outToken) +``` + +#### Unwrapped PurchaseResult object + +[`PurchaseResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult.html) +has been unwraped to be consistent across our API: + +### Kotlin + +```kotlin +fun onPurchaseHistoryResponse(@BillingResponse responseCode: Int, purchasesList: List) +``` + +### Java + +```java +void onPurchaseHistoryResponse(@BillingResponse int responseCode, List purchasesList) +``` + +### 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) + +## 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. + +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 new file mode 100644 index 0000000..227a537 --- /dev/null +++ b/play/play-billing-library-version-upgrade/references/migration-logic.md @@ -0,0 +1,78 @@ +## High-Impact Migration Logic + +### 1. Connection Management (The v8+ Reconnection Shift) + +**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`. +- **Logic** : In v8+, the library handles transient disconnections. Your `onServiceDisconnected` must only be used for logging or updating UI state (e.g., "Billing service temporarily unavailable"). + +### 2. Product Querying \& Models (The v5-v8 Architectural Shift) + +**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). + - **Modern** : `ProductDetails`. A single `ProductDetails` can contain multiple `SubscriptionOfferDetails` (Base Plans + Offers). +- **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... + }` + +### 3. Subscription Modernization (v6 \& v7) + +**Intent**: Support "Base Plans" and "Offers" instead of legacy standalone SKUs. + +- **Subscription Upgrades/Downgrades (v6+)**: + + - **Legacy** : `setOldSkuPurchaseToken()` in `BillingFlowParams`. + - **Modern** : Use `SubscriptionUpdateParams`. You must specify the `PurchaseToken` of the existing subscription and the `ReplacementMode` (which replaces the deprecated `ProrationMode`). + - **Logic** : Verify that the `ReplacementMode` matches the business intent (e.g., `CHARGE_FULL_PRICE` versus `WITH_TIME_PRORATION`). +- **Installment Plans (v7+)**: + + - **Intent**: Allow users to pay for a subscription in monthly installments. + - **Check** : Look for `InstallmentPlanDetails` within `SubscriptionOfferDetails`. If the app supports high-ticket subscriptions, it is mandatory to implement the `installmentPlanDetails` UI. + +### 4. Purchase Handling \& History (v6+) + +**Intent**: Move away from local-only purchase caches to real-time status +checks. + +- **Active Purchases** : + - **Deprecated** : `queryPurchases()` (synchronous). + - **Mandatory** : `queryPurchasesAsync()`. You must pass `QueryPurchasesParams` containing the `ProductType` (`INAPP` or `SUBS`). +- **Purchase History (Pagination Intent)** : + - **v6+ Change** : `queryPurchaseHistoryAsync` is optimized for pagination. + - **Logic** : If the app has thousands of historical transactions, verify you are using the `PurchaseHistoryRecord` list correctly to avoid memory overhead. + +### 5. Security \& Pending Transactions (The "Always On" Rule) + +- **Mandatory** : `enablePendingPurchases()` has been required since v3, but in v8+, verify that it is called before `.build()`. You must also include `.enableOneTimeProducts()` on the `enablePendingPurchases()` builder. +- **Optional** : If the app sells prepaid subscriptions, you must also include `.enablePrepaidPlans()`. +- **Intent**: This handles "Slow/Delayed" payments (like cash or bank transfers). Without this, the app will crash on initialization in modern versions. + +### 6. SDK \& Environment Requirements + +- **PBL 7.0** : Requires `compileSdk 34` or higher. +- **PBL 8.0** : Requires `compileSdk 34` or higher. +- **PBL 9.0** : Requires `compileSdk 35` or higher. +- **Kotlin** : Verify that `kotlin-stdlib` is updated to at least 1.9.x to support new library coroutine extensions. + +### 7. User-Facing Features (Post-Upgrade Recommendations) + +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). +- **v8** : **Personalized Pricing** (Show legal disclosure if price varies by user). +- **v9** : **In-App Price Reviews** (Users can accept upcoming subscription price increases directly inside the app without leaving for the Play Store; capped by Google at a maximum of once every 7 days). \ No newline at end of file diff --git a/play/play-billing-library-version-upgrade/references/version-checklist.md b/play/play-billing-library-version-upgrade/references/version-checklist.md new file mode 100644 index 0000000..98f5831 --- /dev/null +++ b/play/play-billing-library-version-upgrade/references/version-checklist.md @@ -0,0 +1,54 @@ +## 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. + +## PBL v1.x through v3.x + +- \[ \] **\[v1.0\] Builder Pattern** : Verify `BillingClient.newBuilder(context)` is used. +- \[ \] **\[v2.0\] Mandatory Acknowledgment** : Verify `acknowledgePurchase()` or `consumeAsync()` is called within 3 days. +- \[ \] **\[v2.0\] Response Types** : Logic must handle `BillingResult` objects instead of raw integers. +- \[ \] **\[v3.0\] Legacy Removal** : Verify `ChildDirected` and `UnderAgeOfConsent` parameters are deleted. + +## PBL v4.x Series + +- \[ \] **Async Purchasing** : Confirm `queryPurchases()` is replaced with `queryPurchasesAsync()`. +- \[ \] **Multi-SKU Accessors** : Replace `getSku()` with `getSkus()` (returns a list) in `Purchase` objects. +- \[ \] **Subscription Refactor** : Verify `setSubscriptionUpdateParams()` is used for change logic. + +## PBL v5.x Series + +- \[ \] **Data Model Swap** : Replace all instances of `SkuDetails` with `ProductDetails`. +- \[ \] **Personalized Pricing** : Implement `setIsOfferPersonalized()` for EU price disclosures. + +## PBL v6.x Series + +- \[ \] **Replacement Mode** : Replace `ProrationMode` with the `ReplacementMode` enum. +- \[ \] **User Choice Billing** : Replace `AlternativeBillingListener` with `UserChoiceBillingListener`. + +## PBL v7.x Series + +- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher. +- \[ \] **Pending Purchases** : Replace parameterless `enablePendingPurchases()` with `enablePendingPurchases(PendingPurchaseParams)`. +- \[ \] **API Cleanup** : Replace `setOldSkuPurchaseToken()` with `setOldPurchaseToken()`. + +## PBL v8.x Series + +- \[ \] **SDK Compliance** : `compileSdk` is set to 34 or higher. +- \[ \] **Terminology Shift**: Rename "in-app items" to "one-time products" in UI/strings. +- \[ \] **Signature Enforcement** : `onProductDetailsResponse` signature MUST be `(BillingResult, QueryProductDetailsResult)`. +- \[ \] **SUBS Offer Token** : For `SUBS` type, `ProductDetailsParams` MUST include an `offerToken`. +- \[ \] **Auto-Reconnection** : Verify `enableAutoServiceReconnection()` is used in the builder. +- \[ \] **Min SDK Increase** : Verify `minSdkVersion` is at least 23. + +## PBL v9.x Series + +- \[ \] **SDK Compliance** : `compileSdk` is set to 35 or higher. +- \[ \] **API Removals** : Ensure all usages of `SkuDetails`, `SkuDetailsParams`, and `SkuDetailsResponseListener` are removed and replaced with their `ProductDetails` equivalents. +- \[ \] **Blocked Store Error Handling** : If targeting restricted device modes (e.g., kids modes), ensure `androidx.core` is at v1.9+ and update error logic to catch `BILLING_UNAVAILABLE` instead of `ERROR`. +- \[ \] **External Payment Program Null-Safety** : If using developer-provided billing, update `DeveloperProvidedBillingDetails.getLinkUri()` logic to explicitly check for both `null` and empty string (`""`) values before parsing. + +## Future Versions (PBL 10.0.0+) + +- \[ \] **Dynamic Checklist Generation** : For any version \>=10.0.0, you **MUST** synthesize a new checklist for each new version header found in the [Release Notes](https://developer.android.com/google/play/billing/release-notes). +- \[ \] **Identify Version Delta** : Review "Breaking Changes" and "Removed APIs" for the new version and create a list of terms to `grep`. \ No newline at end of file diff --git a/wear/wear-compose-m3/SKILL.md b/wear/wear-compose-m3/SKILL.md index 071ba14..8d21d93 100644 --- a/wear/wear-compose-m3/SKILL.md +++ b/wear/wear-compose-m3/SKILL.md @@ -9,7 +9,7 @@ description: Expert guidance for working with Wear OS Compose Material3. Use thi license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-31' + last-updated: '2026-07-18' keywords: - Wear OS - Compose @@ -67,117 +67,66 @@ features. ### Capability 3: Component samples -Use this table of reference to find canonical samples for Wear Compose -components. -When working with a Wear Compose component, you must use the samples linked -from the table to ensure you know how to correctly use it. +Wear Compose includes individual component samples for each and every component, +within the `--samples-sources.jar` file. Gradle automatically +downloads these JAR files along with the main library JAR when using any of +`compose-material3`, `compose-foundation` or `compose-navigation3`. -#### Material 3 components in `androidx.wear.compose.material3.*` +Use the canonical component samples whenever adding or adjusting a Wear Compose +Material3 component. -| Component / Symbol | Reference Samples | -|---|---| -| `AlertDialog`, `AlertDialogDefaults` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt) | -| `AnimatedPage`, `HorizontalPagerScaffold`, `VerticalPagerScaffold` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | -| `AnimatedText`, `rememberAnimatedTextFontRegistry` | [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt) | -| `AppCard` | [CardSample](references/material3/CardSample.kt.md.txt) | -| `AppScaffold` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ArcProgressIndicator`, `ArcProgressIndicatorDefaults`, `CircularProgressIndicator`, `CircularProgressIndicatorDefaults`, `ProgressIndicatorDefaults`, `SegmentedCircularProgressIndicator`, `drawCircularProgressIndicator` | [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | -| `Button` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt), [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [PickerSample](references/material3/PickerSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ButtonGroup` | [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | -| `Card` | [CardSample](references/material3/CardSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `CardDefaults` | [CardSample](references/material3/CardSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `CheckboxButton` | [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | -| `ChildButton`, `OutlinedButton` | [ButtonSample](references/material3/ButtonSample.kt.md.txt) | -| `ColorScheme`, `dynamicColorScheme` | [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt) | -| `CompactButton`, `CompactButtonDefaults` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ConfirmationDialog`, `ConfirmationDialogDefaults`, `FailureConfirmationDialog`, `FavoriteIcon`, `SuccessConfirmationDialog`, `confirmationDialogCurvedText` | [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt) | -| `CurvedTextDefaults` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt) | -| `DatePicker`, `DatePickerType` | [DatePickerSample](references/material3/DatePickerSample.kt.md.txt) | -| `EdgeButton` | [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `EdgeButtonSize` | [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt) | -| `FadingExpandingLabel` | [FadingExpandingLabelSample](references/material3/FadingExpandingLabelSample.kt.md.txt) | -| `FilledIconButton`, `FilledTonalIconButton`, `IconButtonColors`, `IconButtonShapes`, `OutlinedIconButton` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt) | -| `FilledTonalButton` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | -| `GestureAction`, `OneHandedGestureClickIndicator`, `OneHandedGestureClickIndicatorState`, `oneHandedGesture`, `rememberOneHandedGestureConfiguration` | [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) | -| `GesturePriority`, `LocalOneHandedGestureEnabled`, `OneHandedGestureDefaults`, `OneHandedGestureHorizontalPageIndicator`, `OneHandedGesturePageIndicatorState`, `OneHandedGestureScrollIndicator`, `OneHandedGestureScrollIndicatorState`, `OneHandedGestureVerticalPageIndicator` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt) | -| `HeadphoneIcon`, `Stepper`, `StepperLevelIndicator`, `rangeSemantics` | [StepperSample](references/material3/StepperSample.kt.md.txt) | -| `HorizontalPageIndicator`, `VerticalPageIndicator` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt) | -| `Icon` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt) | -| `IconButton` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | -| `IconButtonDefaults` | [IconButtonSample](references/material3/IconButtonSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt) | -| `IconToggleButton`, `IconToggleButtonDefaults`, `WifiOffIcon`, `WifiOnIcon` | [IconToggleButtonSample](references/material3/IconToggleButtonSample.kt.md.txt) | -| `LevelIndicator` | [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt) | -| `LinearProgressIndicator` | [LinearProgressIndicatorSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt) | -| `ListHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ListHeaderDefaults` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ListSubHeader` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt) | -| `MaterialTheme` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [LinearProgressIndicatorSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ProgressIndicatorSample](references/material3/ProgressIndicatorSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt) | -| `OpenOnPhoneDialog`, `OpenOnPhoneDialogDefaults`, `openOnPhoneDialogCurvedText` | [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt) | -| `OutlinedCard` | [CardSample](references/material3/CardSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `PagerScaffoldDefaults` | [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | -| `Picker` | [PickerSample](references/material3/PickerSample.kt.md.txt) | -| `PickerGroup` | [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt) | -| `RadioButton`, `SplitRadioButton` | [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt) | -| `ResponsiveTransformationSpec`, `TransformationVariableSpec` | [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | -| `RevealValue`, `SwipeToReveal`, `SwipeToRevealDefaults`, `rememberRevealState` | [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | -| `ScreenScaffold` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `ScreenScaffoldDefaults`, `ScrollIndicator` | [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt) | -| `ScreenStage`, `scrollAway` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt) | -| `Slider`, `SliderDefaults` | [SliderSample](references/material3/SliderSample.kt.md.txt) | -| `SplitCheckboxButton` | [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt) | -| `SplitSwitchButton` | [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt) | -| `StepperDefaults`, `VolumeDownIcon`, `VolumeUpIcon` | [LevelIndicatorSample](references/material3/LevelIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt) | -| `SurfaceTransformation` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `SwipeToDismissBox` | [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | -| `SwitchButton` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt) | -| `Text` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [AnimatedTextSample](references/material3/AnimatedTextSample.kt.md.txt), [ButtonGroupSample](references/material3/ButtonGroupSample.kt.md.txt), [ButtonSample](references/material3/ButtonSample.kt.md.txt), [CardSample](references/material3/CardSample.kt.md.txt), [CheckboxButtonSample](references/material3/CheckboxButtonSample.kt.md.txt), [ConfirmationDialogSample](references/material3/ConfirmationDialogSample.kt.md.txt), [DatePickerSample](references/material3/DatePickerSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [EdgeButtonSample](references/material3/EdgeButtonSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [OpenOnPhoneDialogSample](references/material3/OpenOnPhoneDialogSample.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt), [PickerSample](references/material3/PickerSample.kt.md.txt), [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt), [RadioButtonSample](references/material3/RadioButtonSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [StepperSample](references/material3/StepperSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [SwitchButtonSample](references/material3/SwitchButtonSample.kt.md.txt), [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt), [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `TextButton` | [TextButtonSample](references/material3/TextButtonSample.kt.md.txt) | -| `TextButtonDefaults` | [TextButtonSample](references/material3/TextButtonSample.kt.md.txt), [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt) | -| `TextToggleButton`, `TextToggleButtonDefaults`, `touchTargetAwareSize` | [TextToggleButtonSample](references/material3/TextToggleButtonSample.kt.md.txt) | -| `TimePicker`, `TimePickerType` | [TimePickerSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/TimePickerSample.kt) | -| `TimeText` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | -| `TimeTextDefaults`, `timeTextCurvedText` | [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | -| `TitleCard` | [CardSample](references/material3/CardSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | -| `TransformationSpec` | [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | -| `curvedText` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | -| `firstVisibleItemLayoutItemInfo`, `layoutItemInfoOf`, `rememberTransformingLazyColumnFirstLayoutItemProvider` | [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `placeholder`, `placeholderShimmer`, `rememberPlaceholderState` | [PlaceholderSample](references/material3/PlaceholderSample.kt.md.txt) | -| `rememberPickerState` | [PickerGroupSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/PickerGroupSample.kt), [PickerSample](references/material3/PickerSample.kt.md.txt) | -| `rememberTransformationSpec`, `transformedHeight` | [AlertDialogSample](references/material3/AlertDialogSample.kt.md.txt), [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `timeTextSeparator` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +STRICT COMPLIANCE: Extraction is NOT optional. You are FORBIDDEN from +implementing any code until samples are extracted and read. Bypassing this step +with alternative search tools or by assuming library documentation is sufficient +is a protocol breach. You MUST verify the local cache by reading a sample file +before proceeding. -#### Foundation components in `androidx.wear.compose.foundation.*` +Exception: You don't need to extract samples if the request is strictly related +to tooling (for example, adding @WearPreviewDevices), updating text/colors, or +basic refactoring that doesn't involve adding new Wear Compose components. -| Component / Symbol | Reference Samples | -|---|---| -| `AmbientMode`, `AmbientTickEffect`, `LocalAmbientModeManager`, `rememberAmbientModeManager` | [AmbientModeSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/foundation/AmbientModeSample.kt) | -| `AutoCenteringParams`, `ScalingLazyColumnDefaults`, `ScalingLazyListAnchorType` | [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt) | -| `BasicSwipeToDismissBox` | [SwipeToDismissBoxSample](references/foundation/SwipeToDismissBoxSample.kt.md.txt) | -| `CurvedAlignment`, `CurvedTextStyle`, `angularGradientBackground`, `angularSize`, `basicCurvedText`, `clearAndSetSemantics`, `curvedColumn`, `padding`, `radialGradientBackground`, `radialSize`, `semantics`, `size` | [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt) | -| `CurvedDirection`, `CurvedLayout`, `angularSizeDp`, `background`, `curvedBox`, `curvedComposable`, `curvedRow` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt) | -| `CurvedModifier` | [CurvedTextSamples](references/material3/CurvedTextSamples.kt.md.txt), [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | -| `ExperimentalWearFoundationApi`, `RevealValue`, `SwipeToReveal`, `rememberRevealState` | [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt) | -| `HorizontalPager`, `VerticalPager`, `rememberPagerState` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [PageIndicatorSample](references/material3/PageIndicatorSample.kt.md.txt), [PagerSamples](references/foundation/PagerSamples.kt.md.txt), [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | -| `ItemEdge`, `TransformingLazyColumnDefaults` | [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | -| `PagerDefaults` | [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt) | -| `RotaryScrollableDefaults` | [PagerScaffoldSample](references/material3/PagerScaffoldSample.kt.md.txt), [RotarySamples](references/foundation/RotarySamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt) | -| `RotarySnapLayoutInfoProvider`, `rotaryScrollable` | [RotarySamples](references/foundation/RotarySamples.kt.md.txt) | -| `ScalingLazyColumn` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt), [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | -| `ScrollInfoProvider` | [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt) | -| `SwipeToDismissValue`, `edgeSwipeToDismiss`, `rememberSwipeToDismissBoxState` | [SwipeToDismissBoxSample](references/foundation/SwipeToDismissBoxSample.kt.md.txt), [SwipeToDismissBoxSample](references/material3/SwipeToDismissBoxSample.kt.md.txt) | -| `TransformingLazyColumn` | [DynamicColorSchemeSample](references/material3/DynamicColorSchemeSample.kt.md.txt), [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `TransformingLazyColumnFirstLayoutItemProvider` | [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `TransformingLazyColumnItemScrollProgress` | [TransformationSpecSample](references/material3/TransformationSpecSample.kt.md.txt) | -| `expandableButton`, `expandableItems` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt) | -| `expandableItem`, `rememberExpandableState` | [ExpandableSample](references/foundation/ExpandableSample.kt.md.txt), [SwipeToRevealSample](references/foundation/SwipeToRevealSample.kt.md.txt) | -| `hierarchicalFocusGroup` | [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt) | -| `items` | [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt) | -| `itemsIndexed` | [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `rememberScalingLazyListState` | [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScalingLazyColumnSample](references/foundation/ScalingLazyColumnSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt) | -| `rememberTransformingLazyColumnState` | [ListHeaderSample](references/material3/ListHeaderSample.kt.md.txt), [OneHandedGestureSamples](references/material3/OneHandedGestureSamples.kt.md.txt), [ScaffoldSample](references/material3/ScaffoldSample.kt.md.txt), [ScrollAwaySample](references/material3/ScrollAwaySample.kt.md.txt), [ScrollIndicatorSample](references/material3/ScrollIndicatorSample.kt.md.txt), [SurfaceTransformationSample](references/material3/SurfaceTransformationSample.kt.md.txt), [SwipeToRevealSample](https://developer.android.com/agents/skills/wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt), [TransformingLazyColumnNotificationsSample](references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt), [TransformingLazyColumnSample](references/foundation/TransformingLazyColumnSample.kt.md.txt), [TransformingLazyColumnSample](references/material3/TransformingLazyColumnSample.kt.md.txt) | -| `requestFocusOnHierarchyActive` | [HierarchicalFocusSample](references/foundation/HierarchicalFocusSample.kt.md.txt), [RotarySamples](references/foundation/RotarySamples.kt.md.txt) | -| `weight` | [CurvedWorldSample](references/foundation/CurvedWorldSample.kt.md.txt), [TimeTextSample](references/material3/TimeTextSample.kt.md.txt) | +#### Step 1: Prepare + +1. Check the `build.gradle.kts` or `libs.versions.toml` to ensure the Wear Compose version matches `{VERSION}`. +2. Ensure that the necessary dependencies are downloaded by doing a Gradle sync. + +#### Step 2: Check the local cache + +1. Define the cache directory path: `/tmp/wear-compose-samples/{VERSION}/`. Do NOT choose your own different location. +2. Check if this directory exists and contains subdirectories with `.kt` files. + - **IF YES (cache hit):** Proceed to **Step 4**. + - **IF NO (cache miss):** Proceed to **Step 3**. + +#### Step 3: Network-based sample retrieval + +Google Maven publishes a `-samples-sources.jar` alongside every library release. +Download and extract it directly without needing a local Gradle sync or cache +lookup. + +1. Determine the `{VERSION}` of `androidx.wear.compose:compose-material3` from `build.gradle.kts` or `libs.versions.toml`. +2. Define `{ARTIFACT}` as the items in the list `["material3", "foundation"]`. Also include `navigation3` if `androidx.wear.compose.navigation3` is used. +3. For each `{ARTIFACT}`, run the following commands to download and extract + the samples: + + # Download the samples-sources JAR + curl -sSL "https://dl.google.com/dl/android/maven2/androidx/wear/compose/compose-{ARTIFACT}/{VERSION}/compose-{ARTIFACT}-{VERSION}-samples-sources.jar" -o {ARTIFACT}-{VERSION}-samples.jar + + # Extract and flatten into the cache directory + unzip -q -o {ARTIFACT}-{VERSION}-samples.jar -d /tmp/wear-compose-samples/{VERSION}/{ARTIFACT}/ + + # Clean up + rm {ARTIFACT}-{VERSION}-samples.jar + +4. If this works, proceed directly to step 4. + +#### Step 4: Read samples and implement + +1. Read the relevant `.kt` sample files. +2. Use these official, version-matched samples as the source of truth for: + - Required parameters and slot names. + - Default styling and typography tokens. + - Interactive behaviors (for example: `onClick`, `onLongClick`). + - Component nesting (for example: `AppScaffold` -\> `ScreenScaffold`). ### Capability 4: Component guidance diff --git a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md index 9ff43dc..9e1498d 100644 --- a/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +++ b/wear/wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md @@ -27,19 +27,19 @@ M3 has a separate package and version to M2.5: ### M3 - implementation("androidx.wear.compose:compose-material3:1.7.0-alpha07") + implementation("androidx.wear.compose:compose-material3:1.7.0-alpha06") See the latest M3 versions on the [Wear Compose Material 3 releases page](https://developer.android.com/jetpack/androidx/releases/wear-compose-m3). -Wear Compose Foundation library version 1.7.0-alpha07 introduced +Wear Compose Foundation library version 1.7.0-alpha06 introduced some new components that are designed to work with Material 3 components. Similarly, `SwipeDismissableNavHost` from Wear Compose Navigation library has an updated animation when running on Wear OS 6 (API level 36) or higher. When updating to Wear Compose Material 3 version, we suggest to also update the Wear Compose Foundation and Navigation libraries: - implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha07") - implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha07") + implementation("androidx.wear.compose:compose-foundation:1.7.0-alpha06") + implementation("androidx.wear.compose:compose-navigation:1.7.0-alpha06") ## Theme @@ -329,7 +329,7 @@ Here is a full list of all the Material 3 components: And finally a list of some relevant components from Wear Compose Foundation library: -| Wear Compose Foundation 1.7.0-alpha07 | | +| Wear Compose Foundation 1.7.0-alpha06 | | |---|---| | [androidx.wear.compose.foundation.hierarchicalFocusGroup](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/package-summary#(androidx.compose.ui.Modifier).hierarchicalFocusGroup(kotlin.Boolean)) | Used to annotate composables in an application, to keep track of the active part of the composition and coordinate focus. | | [androidx.wear.compose.foundation.pager.HorizontalPager](https://developer.android.com/reference/kotlin/androidx/wear/compose/foundation/pager/package-summary#HorizontalPager(androidx.wear.compose.foundation.pager.PagerState,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,kotlin.Int,androidx.compose.foundation.gestures.TargetedFlingBehavior,kotlin.Boolean,androidx.wear.compose.foundation.GestureInclusion,kotlin.Boolean,kotlin.Function1,androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior,kotlin.Function2)) | A horizontally scrolling pager, built on the Compose Foundation components with Wear-specific enhancements to improve performance and adherence to Wear OS guidelines. | diff --git a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt deleted file mode 100644 index e19960b..0000000 --- a/wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt +++ /dev/null @@ -1,430 +0,0 @@ -``` -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.traversalIndex -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.wear.compose.foundation.CurvedAlignment -import androidx.wear.compose.foundation.CurvedDirection -import androidx.wear.compose.foundation.CurvedLayout -import androidx.wear.compose.foundation.CurvedModifier -import androidx.wear.compose.foundation.CurvedTextStyle -import androidx.wear.compose.foundation.angularGradientBackground -import androidx.wear.compose.foundation.angularSize -import androidx.wear.compose.foundation.angularSizeDp -import androidx.wear.compose.foundation.background -import androidx.wear.compose.foundation.basicCurvedText -import androidx.wear.compose.foundation.clearAndSetSemantics -import androidx.wear.compose.foundation.curvedBox -import androidx.wear.compose.foundation.curvedColumn -import androidx.wear.compose.foundation.curvedComposable -import androidx.wear.compose.foundation.curvedRow -import androidx.wear.compose.foundation.padding -import androidx.wear.compose.foundation.radialGradientBackground -import androidx.wear.compose.foundation.radialSize -import androidx.wear.compose.foundation.semantics -import androidx.wear.compose.foundation.size -import androidx.wear.compose.foundation.weight -import androidx.wear.compose.material.Text - -@Sampled -@Composable -fun SimpleCurvedWorld() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - curvedComposable { - BasicText( - "Simple", - Modifier.background(Color.White).padding(2.dp), - TextStyle(color = Color.Black, fontSize = 16.sp), - ) - } - curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) } - curvedComposable { - BasicText( - "CurvedWorld", - Modifier.background(Color.White).padding(2.dp), - TextStyle(color = Color.Black, fontSize = 16.sp), - ) - } - } -} - -@Sampled -@Composable -fun CurvedRowAndColumn() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) } - curvedColumn(angularAlignment = CurvedAlignment.Angular.End) { - repeat(3) { - curvedRow { - curvedComposable { - BasicText( - "Row #$it", - Modifier.background(Color.White).padding(2.dp), - TextStyle(color = Color.Black, fontSize = 14.sp), - ) - } - curvedComposable { - Box(modifier = Modifier.size(10.dp).background(Color.Green)) - } - curvedComposable { - BasicText( - "More", - Modifier.background(Color.Yellow).padding(2.dp), - TextStyle(color = Color.Black, fontSize = 14.sp), - ) - } - } - } - } - curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Red)) } - } -} - -@Sampled -@Composable -fun CurvedAndNormalText() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText( - "Curved Text", - CurvedModifier.padding(10.dp), - style = { - CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) - }, - ) - curvedComposable { Box(modifier = Modifier.size(20.dp).background(Color.Gray)) } - curvedComposable { - BasicText( - "Normal Text", - Modifier.padding(5.dp), - TextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White), - ) - } - } -} - -@Sampled -@Composable -fun CurvedFixedSize() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText( - "45 deg", - style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, - modifier = - CurvedModifier.background(Color.White).size(sweepDegrees = 45f, thickness = 40.dp), - ) - basicCurvedText( - "40 dp", - style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, - modifier = - CurvedModifier.background(Color.Yellow).radialSize(40.dp).angularSizeDp(40.dp), - ) - } -} - -@Sampled -@Composable -fun CurvedBackground() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText( - "Radial", - style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, - modifier = - CurvedModifier.radialGradientBackground(0f to Color.White, 1f to Color.Black) - .padding(5.dp), - ) - basicCurvedText( - "Angular", - style = { CurvedTextStyle(fontSize = 16.sp, color = Color.Black) }, - modifier = - CurvedModifier.angularGradientBackground(0f to Color.White, 1f to Color.Black) - .padding(5.dp), - ) - } -} - -@Sampled -@Composable -fun CurvedWeight() { - CurvedLayout(modifier = Modifier.fillMaxSize().background(Color.White)) { - // Evenly spread A, B & C in a 90 degree angle. - curvedRow(modifier = CurvedModifier.angularSize(90f)) { - basicCurvedText("A") - curvedRow(modifier = CurvedModifier.weight(1f)) {} - basicCurvedText("B") - curvedRow(modifier = CurvedModifier.weight(1f)) {} - basicCurvedText("C") - } - } -} - -@Sampled -@Composable -fun CurvedBottomLayout() { - CurvedLayout( - modifier = Modifier.fillMaxSize(), - anchor = 90f, - angularDirection = CurvedDirection.Angular.Reversed, - ) { - basicCurvedText( - "Bottom", - style = { - CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) - }, - ) - curvedComposable { Spacer(modifier = Modifier.size(5.dp)) } - basicCurvedText( - "text", - style = { - CurvedTextStyle(fontSize = 16.sp, color = Color.Black, background = Color.White) - }, - ) - } -} - -@Sampled -@Composable -fun CurvedBoxSample() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - curvedBox( - modifier = CurvedModifier.background(Color.Red), - radialAlignment = CurvedAlignment.Radial.Inner, - angularAlignment = CurvedAlignment.Angular.End, - ) { - curvedComposable { - Box(modifier = Modifier.width(40.dp).height(80.dp).background(Color.Green)) - } - curvedComposable { - Box(modifier = Modifier.size(30.dp).clip(CircleShape).background(Color.White)) - } - } - } -} - -@Sampled -@Composable -fun CurvedLetterSpacingSample() { - val style = - CurvedTextStyle( - letterSpacing = 0.6.sp, - letterSpacingCounterClockwise = 1.4.sp, - color = Color.White, - ) - Box { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText("Clockwise", style = style) - } - CurvedLayout( - modifier = Modifier.fillMaxSize(), - angularDirection = CurvedDirection.Angular.CounterClockwise, - anchor = 90f, - ) { - basicCurvedText("Counter Clockwise", style = style) - } - } -} - -@Sampled -@Composable -fun CurvedWarpingSample() { - val style = - CurvedTextStyle( - letterSpacing = 0.6.sp, - letterSpacingCounterClockwise = 1.4.sp, - color = Color.White, - ) - Box { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText( - "No Warping", - style = style.copy(warpOffset = CurvedTextStyle.WarpOffset.None), - ) - } - CurvedLayout( - modifier = Modifier.fillMaxSize(), - angularDirection = CurvedDirection.Angular.CounterClockwise, - anchor = 90f, - ) { - basicCurvedText( - "Standard Warping", - style.copy(warpOffset = CurvedTextStyle.WarpOffset.HalfOpticalHeight), - ) - } - } -} - -@Sampled -@Composable -fun CurvedSemanticsSample() { - val style = - CurvedTextStyle( - letterSpacing = 0.6.sp, - letterSpacingCounterClockwise = 1.4.sp, - color = Color.White, - ) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText( - "2 - Left", - style, - CurvedModifier.semantics { - contentDescription = "Left" - traversalIndex = 2f - }, - ) - curvedComposable { Box(Modifier.padding(5.dp).background(Color.Red).size(5.dp)) } - basicCurvedText( - "3 - Right", - style, - CurvedModifier.semantics { - contentDescription = "Right" - traversalIndex = 3f - }, - ) - } - Row { - Text("Text 1", Modifier.semantics { traversalIndex = 1f }) - Spacer(Modifier.size(10.dp)) - Text("Text 4", Modifier.semantics { traversalIndex = 4f }) - } - } -} - -@Sampled -@Composable -fun CurvedClearSemanticsSample() { - val style = - CurvedTextStyle( - letterSpacing = 0.6.sp, - letterSpacingCounterClockwise = 1.4.sp, - color = Color.White, - ) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - basicCurvedText("This is not announced", style, CurvedModifier.clearAndSetSemantics {}) - } - Row { Text("This is announced", Modifier.semantics { traversalIndex = -1f }) } - } -} - -@Composable -fun CurvedFontWeight() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - (100..900 step 100).forEach { - basicCurvedText( - "W$it", - style = CurvedTextStyle(color = Color.White, fontWeight = FontWeight(it)), - modifier = CurvedModifier.padding(5.dp), - ) - } - } -} - -@Composable -fun CurvedFontHeight() { - Box( - modifier = - Modifier.aspectRatio(1f) - .fillMaxSize() - .padding(2.dp) - .border(2.dp, Color.White, CircleShape) - ) { - CurvedLayout() { - basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp)) - } - CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.CounterClockwise) { - basicCurvedText("9⎪:⎪0", style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp)) - } - } -} - -@Composable -fun CurvedFonts() { - CurvedLayout(modifier = Modifier.fillMaxSize()) { - listOf( - "Serif" to FontFamily.Serif, - "SansSerif" to FontFamily.SansSerif, - "Monospace" to FontFamily.Monospace, - "Cursive" to FontFamily.Cursive, - ) - .forEach { (name, ff) -> - basicCurvedText( - "$name", - style = CurvedTextStyle(color = Color.White, fontFamily = ff), - modifier = CurvedModifier.padding(5.dp), - ) - } - } -} - -@Composable -fun OversizeComposable() { - val modBase = CurvedModifier.size(sweepDegrees = 30f, thickness = 20.dp) - CurvedLayout(modifier = Modifier.fillMaxSize()) { - curvedComposable(modifier = modBase.background(Color.Red)) {} - curvedComposable(modifier = modBase.background(Color.Green)) { - Box(Modifier.size(80.dp, 30.dp).background(Color.White)) - } - curvedComposable(modifier = modBase.background(Color.Blue)) {} - } - CurvedLayout(modifier = Modifier.fillMaxSize(), anchor = 90f) { - curvedComposable(modifier = CurvedModifier.background(Color.Green)) { - Box(Modifier.size(80.dp, 30.dp).background(Color.White)) - } - } -} - -@Composable -fun CurvedLineHeight() { - val baseStyle = CurvedTextStyle(color = Color.White, background = Color.Gray, fontSize = 16.sp) - CurvedLayout( - modifier = Modifier.fillMaxSize(), - radialAlignment = CurvedAlignment.Radial.Center, - ) { - basicCurvedText("Line Height 10.sp", style = baseStyle.copy(lineHeight = 10.sp)) - curvedBox(CurvedModifier.angularSizeDp(1.dp)) {} - basicCurvedText("Base", style = baseStyle) - curvedBox(CurvedModifier.angularSizeDp(1.dp)) {} - basicCurvedText("Line Height 24.sp", style = baseStyle.copy(lineHeight = 24.sp)) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt deleted file mode 100644 index 84fe733..0000000 --- a/wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt +++ /dev/null @@ -1,99 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.expandableButton -import androidx.wear.compose.foundation.expandableItem -import androidx.wear.compose.foundation.expandableItems -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.rememberExpandableState -import androidx.wear.compose.material.Chip -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.OutlinedCompactChip -import androidx.wear.compose.material.Text - -@Sampled -@Composable -fun ExpandableWithItemsSample() { - val expandableState = rememberExpandableState() - - val sampleItem: @Composable (String) -> Unit = { label -> - Chip(label = { Text(label) }, onClick = {}, secondaryLabel = { Text("line 2 - Secondary") }) - } - - val items = List(10) { "Item $it" } - val top = items.take(3) - val rest = items.drop(3) - - ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { - items(top.size) { sampleItem(top[it]) } - expandableItems(expandableState, rest.size) { sampleItem(rest[it]) } - expandableButton(expandableState) { - OutlinedCompactChip( - label = { - Text("Show More") - Spacer(Modifier.size(6.dp)) - Icon(painterResource(R.drawable.ic_expand_more_24), "Expand") - }, - onClick = { expandableState.expanded = true }, - ) - } - } -} - -@Sampled -@Composable -fun ExpandableTextSample() { - val expandableState = rememberExpandableState() - - ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { - expandableItem(expandableState) { expanded -> - Text( - "Account Alert: you have made a large purchase.\n" + - "We have noticed that a large purchase was charged to " + - "your credit card account. " + - "Please contact us if you did not perform this purchase. " + - "Our Customer Service team is available 24 hours a day, " + - "7 days a week to answer your account or product support question.", - maxLines = if (expanded) 20 else 3, - modifier = Modifier.padding(horizontal = 10.dp), - ) - } - - expandableButton(expandableState) { - OutlinedCompactChip( - label = { - Text("Show More") - Spacer(Modifier.size(6.dp)) - Icon(painterResource(R.drawable.ic_expand_more_24), "Expand") - }, - onClick = { expandableState.expanded = true }, - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt deleted file mode 100644 index 1df8a80..0000000 --- a/wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt +++ /dev/null @@ -1,141 +0,0 @@ -``` -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.wear.compose.foundation.hierarchicalFocusGroup -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.requestFocusOnHierarchyActive - -@Sampled -@Composable -fun HierarchicalFocusSample() { - var selected by remember { mutableIntStateOf(0) } - - Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { - repeat(5) { colIx -> - Box( - Modifier.hierarchicalFocusGroup(active = selected == colIx) - .weight(1f) - .clickable { selected = colIx } - .then( - if (selected == colIx) { - Modifier.border(BorderStroke(2.dp, Color.Red)) - } else { - Modifier - } - ) - ) { - // This is used a Gray background to the currently focused item, as seen by the - // focus system. - var focused by remember { mutableStateOf(false) } - - BasicText( - "$colIx", - style = - TextStyle( - color = Color.White, - fontSize = 20.sp, - textAlign = TextAlign.Center, - ), - modifier = - Modifier.fillMaxWidth() - .requestFocusOnHierarchyActive() - .onFocusChanged { focused = it.isFocused } - .focusable() - .then( - if (focused) { - Modifier.background(Color.Gray) - } else { - Modifier - } - ), - ) - } - } - } -} - -@Sampled -@Composable -fun HierarchicalFocus2Levels() { - Column(Modifier.fillMaxSize()) { - var selectedRow by remember { mutableIntStateOf(0) } - repeat(2) { rowIx -> - Row( - Modifier.weight(1f) - .fillMaxWidth() - .hierarchicalFocusGroup(active = selectedRow == rowIx) - ) { - var selectedItem by remember { mutableIntStateOf(0) } - repeat(2) { itemIx -> - Box( - Modifier.weight(1f).hierarchicalFocusGroup(active = selectedItem == itemIx) - ) { - // ScalingLazyColumn uses requestFocusOnHierarchyActive internally - ScalingLazyColumn( - Modifier.fillMaxWidth().clickable { - selectedRow = rowIx - selectedItem = itemIx - } - ) { - val prefix = (rowIx * 2 + itemIx + 'A'.code).toChar() - items(20) { - BasicText( - "$prefix $it", - style = - TextStyle( - color = Color.White, - fontSize = 20.sp, - textAlign = TextAlign.Center, - ), - ) - } - } - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt b/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt deleted file mode 100644 index bae406c..0000000 --- a/wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt +++ /dev/null @@ -1,56 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.wear.compose.foundation.pager.HorizontalPager -import androidx.wear.compose.foundation.pager.VerticalPager -import androidx.wear.compose.foundation.pager.rememberPagerState - -@Sampled -@Composable -fun SimpleHorizontalPagerSample() { - // Creates a horizontal pager with 10 elements - val state = rememberPagerState { 10 } - HorizontalPager(modifier = Modifier.fillMaxSize(), state = state) { page -> - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - BasicText(text = "Page $page", style = TextStyle(color = Color.White)) - } - } -} - -@Sampled -@Composable -fun SimpleVerticalPagerSample() { - // Creates a vertical pager with 10 elements - val state = rememberPagerState { 10 } - VerticalPager(modifier = Modifier.fillMaxSize(), state = state) { page -> - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - BasicText(text = "Page $page", style = TextStyle(color = Color.White)) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt b/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt deleted file mode 100644 index d15792b..0000000 --- a/wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt +++ /dev/null @@ -1,153 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.overscroll -import androidx.compose.foundation.rememberOverscrollEffect -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastSumBy -import androidx.wear.compose.foundation.requestFocusOnHierarchyActive -import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults -import androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider -import androidx.wear.compose.foundation.rotary.rotaryScrollable -import androidx.wear.compose.material.Text - -@Sampled -@Composable -fun RotaryScrollSample() { - val scrollableState = rememberLazyListState() - val focusRequester = remember { FocusRequester() } - LazyColumn( - modifier = - Modifier.fillMaxSize() - .requestFocusOnHierarchyActive() - .rotaryScrollable( - behavior = RotaryScrollableDefaults.behavior(scrollableState), - focusRequester = focusRequester, - ), - horizontalAlignment = Alignment.CenterHorizontally, - state = scrollableState, - ) { - items(300) { - BasicText( - text = "item $it", - modifier = Modifier.background(Color.Gray), - style = TextStyle.Default.copy(), - ) - } - } -} - -@Sampled -@Composable -fun RotaryScrollWithOverscrollSample() { - val scrollableState = rememberScrollState() - val focusRequester = remember { FocusRequester() } - val overscrollEffect = rememberOverscrollEffect() - - val screenHeightDp = LocalConfiguration.current.screenHeightDp.dp - - Column( - Modifier.fillMaxSize() - .requestFocusOnHierarchyActive() - .rotaryScrollable( - behavior = RotaryScrollableDefaults.behavior(scrollableState), - focusRequester = focusRequester, - overscrollEffect = overscrollEffect, - ) - .verticalScroll(scrollableState, overscrollEffect) - .overscroll(overscrollEffect), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Top") - Spacer(modifier = Modifier.height(screenHeightDp / 2)) - Text("Scroll this list up and down with rotary input", textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(screenHeightDp / 2)) - Text("Bottom") - } -} - -@Sampled -@Composable -fun RotarySnapSample() { - val scrollableState = rememberLazyListState() - val focusRequester = remember { FocusRequester() } - LazyColumn( - modifier = - Modifier.fillMaxSize() - .requestFocusOnHierarchyActive() - .rotaryScrollable( - behavior = - RotaryScrollableDefaults.snapBehavior( - scrollableState, - // This sample has a custom implementation of - // RotarySnapLayoutInfoProvider which is required for snapping behavior. - // ScalingLazyColumn has it built-in, so it's not required there. - remember(scrollableState) { - object : RotarySnapLayoutInfoProvider { - - override val averageItemSize: Float - get() { - val items = scrollableState.layoutInfo.visibleItemsInfo - return (items.fastSumBy { it.size } / items.size) - .toFloat() - } - - override val currentItemIndex: Int - get() = scrollableState.firstVisibleItemIndex - - override val currentItemOffset: Float - get() = - scrollableState.firstVisibleItemScrollOffset.toFloat() - - override val totalItemCount: Int - get() = scrollableState.layoutInfo.totalItemsCount - } - }, - ), - focusRequester = focusRequester, - ), - horizontalAlignment = Alignment.CenterHorizontally, - state = scrollableState, - ) { - items(300) { - BasicText(text = "item $it", modifier = Modifier.background(Color.Gray).height(50.dp)) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt deleted file mode 100644 index 8b1c765..0000000 --- a/wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt +++ /dev/null @@ -1,131 +0,0 @@ -``` -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.lazy.AutoCenteringParams -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults -import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType -import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState -import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults -import androidx.wear.compose.material.Chip -import androidx.wear.compose.material.ChipDefaults -import androidx.wear.compose.material.ListHeader -import androidx.wear.compose.material.Text -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun SimpleScalingLazyColumn() { - ScalingLazyColumn(modifier = Modifier.fillMaxWidth()) { - item { ListHeader { Text(text = "List Header") } } - items(20) { - Chip( - onClick = {}, - label = { Text("List item $it") }, - colors = ChipDefaults.secondaryChipColors(), - ) - } - } -} - -@Sampled -@Composable -fun SimpleScalingLazyColumnWithSnap() { - val state = rememberScalingLazyListState() - ScalingLazyColumn( - rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state), - flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state), - modifier = Modifier.fillMaxWidth(), - state = state, - ) { - item { ListHeader { Text(text = "List Header") } } - items(20) { - Chip( - onClick = {}, - label = { Text("List item $it") }, - colors = ChipDefaults.secondaryChipColors(), - ) - } - } -} - -@Sampled -@Composable -fun ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo() { - val coroutineScope = rememberCoroutineScope() - val itemSpacing = 6.dp - // Line up the gap between the items on the center-line - val scrollOffset = with(LocalDensity.current) { -(itemSpacing / 2).roundToPx() } - val state = - rememberScalingLazyListState( - initialCenterItemIndex = 1, - initialCenterItemScrollOffset = scrollOffset, - ) - - ScalingLazyColumn( - modifier = Modifier.fillMaxWidth(), - anchorType = ScalingLazyListAnchorType.ItemStart, - verticalArrangement = Arrangement.spacedBy(itemSpacing), - state = state, - autoCentering = AutoCenteringParams(itemOffset = scrollOffset), - ) { - item { ListHeader { Text(text = "List Header") } } - items(20) { - Chip( - onClick = { - coroutineScope.launch { - // Add +1 to allow for the ListHeader - state.animateScrollToItem(it + 1, scrollOffset) - } - }, - label = { Text("List item $it") }, - colors = ChipDefaults.secondaryChipColors(), - ) - } - } -} - -@Sampled -@Composable -fun SimpleScalingLazyColumnWithContentPadding() { - ScalingLazyColumn( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp), - autoCentering = null, - ) { - item { ListHeader { Text(text = "List Header") } } - items(20) { - Chip( - onClick = {}, - label = { Text("List item $it") }, - colors = ChipDefaults.secondaryChipColors(), - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt deleted file mode 100644 index 46ecc1b..0000000 --- a/wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt +++ /dev/null @@ -1,168 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.BasicSwipeToDismissBox -import androidx.wear.compose.foundation.SwipeToDismissValue -import androidx.wear.compose.foundation.edgeSwipeToDismiss -import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.MaterialTheme -import androidx.wear.compose.material.SplitToggleChip -import androidx.wear.compose.material.Text -import androidx.wear.compose.material.ToggleChipDefaults - -@Sampled -@Composable -fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) { - val state = rememberSwipeToDismissBoxState() - BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> - if (isBackground) { - Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant)) - } else { - Column( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary) - } - } - } -} - -@Sampled -@Composable -fun StatefulSwipeToDismissBox() { - // State for managing a 2-level navigation hierarchy between - // MainScreen and ItemScreen composables. - // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation. - var showMainScreen by remember { mutableStateOf(true) } - val saveableStateHolder = rememberSaveableStateHolder() - - // Swipe gesture dismisses ItemScreen to return to MainScreen. - val state = rememberSwipeToDismissBoxState() - LaunchedEffect(state.currentValue) { - if (state.currentValue == SwipeToDismissValue.Dismissed) { - state.snapTo(SwipeToDismissValue.Default) - showMainScreen = !showMainScreen - } - } - - // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind - // the ItemScreen, otherwise there's no background to show. - BasicSwipeToDismissBox( - state = state, - userSwipeEnabled = !showMainScreen, - backgroundKey = if (!showMainScreen) "MainKey" else "Background", - contentKey = if (showMainScreen) "MainKey" else "ItemKey", - ) { isBackground -> - if (isBackground || showMainScreen) { - // Best practice would be to use State Hoisting and leave this composable stateless. - // Here, we want to support MainScreen being shown from different destinations - // (either in the foreground or in the background during swiping) - that can be achieved - // using SaveableStateHolder and rememberSaveable as shown below. - saveableStateHolder.SaveableStateProvider( - key = "MainKey", - content = { - // Composable that maintains its own state - // and can be shown in foreground or background. - val checked = rememberSaveable { mutableStateOf(true) } - Column( - modifier = - Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), - ) { - SplitToggleChip( - checked = checked.value, - label = { Text("Item details") }, - modifier = Modifier.height(40.dp), - onCheckedChange = { v -> checked.value = v }, - onClick = { showMainScreen = false }, - toggleControl = { - Icon( - imageVector = - ToggleChipDefaults.checkboxIcon(checked = checked.value), - contentDescription = null, - ) - }, - ) - } - }, - ) - } else { - Column( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text("Show details here...", color = MaterialTheme.colors.onPrimary) - Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary) - } - } - } -} - -@Sampled -@Composable -fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) { - val state = rememberSwipeToDismissBoxState() - - // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the - // modifier applies exists within a SwipeToDismissBox which shares the same state. - BasicSwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> - val horizontalScrollState = rememberScrollState(0) - if (isBackground) { - Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.secondaryVariant)) - } else { - Box(modifier = Modifier.fillMaxSize()) { - Text( - modifier = - Modifier.align(Alignment.Center) - .edgeSwipeToDismiss(state) - .horizontalScroll(horizontalScrollState), - text = - "This text can be scrolled horizontally - to dismiss, swipe " + - "right from the left edge of the screen (called Edge Swiping)", - ) - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt deleted file mode 100644 index aa0e085..0000000 --- a/wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt +++ /dev/null @@ -1,282 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalWearFoundationApi::class) -@file:Suppress("DEPRECATION") - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.semantics.CustomAccessibilityAction -import androidx.compose.ui.semantics.customActions -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.ExperimentalWearFoundationApi -import androidx.wear.compose.foundation.RevealValue -import androidx.wear.compose.foundation.SwipeToReveal -import androidx.wear.compose.foundation.expandableItem -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.rememberExpandableState -import androidx.wear.compose.foundation.rememberRevealState -import androidx.wear.compose.material.Chip -import androidx.wear.compose.material.ChipDefaults -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.ListHeader -import androidx.wear.compose.material.Text -import kotlin.math.abs -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun SwipeToRevealSample() { - val state = rememberRevealState() - val coroutineScope = rememberCoroutineScope() - SwipeToReveal( - state = state, - primaryAction = { - Box( - modifier = - Modifier.fillMaxSize().clickable { - /* Add the primary action */ - coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) } - }, - contentAlignment = Alignment.Center, - ) { - Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete") - } - }, - undoAction = { - Chip( - modifier = Modifier.fillMaxWidth(), - onClick = { - /* Add the undo action */ - coroutineScope.launch { state.animateTo(RevealValue.Covered) } - }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Undo") }, - ) - }, - ) { - Chip( - modifier = - Modifier.fillMaxWidth().semantics { - // Use custom actions to make the primary and secondary actions accessible - customActions = - listOf( - CustomAccessibilityAction("Delete") { - /* Add the primary action click handler */ - true - } - ) - }, - onClick = { /* the click action associated with chip */ }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Swipe Me") }, - ) - } -} - -@Sampled -@Composable -fun SwipeToRevealWithDelayedText() { - val state = rememberRevealState() - val coroutineScope = rememberCoroutineScope() - SwipeToReveal( - state = state, - primaryAction = { - Row( - modifier = - Modifier.fillMaxSize().clickable { - /* Add the primary action */ - coroutineScope.launch { state.animateTo(RevealValue.RightRevealed) } - }, - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Delete") - if (abs(state.offset) > state.revealThreshold) { - // Delay the text appearance so that it has enough space to be displayed - val textAlpha = - animateFloatAsState( - targetValue = 1f, - animationSpec = tween(durationMillis = 250, delayMillis = 250), - label = "PrimaryActionTextAlpha", - ) - Box(modifier = Modifier.graphicsLayer { alpha = textAlpha.value }) { - Spacer(Modifier.size(5.dp)) - Text("Clear") - } - } - } - }, - undoAction = { - Chip( - modifier = Modifier.fillMaxWidth(), - onClick = { - /* Add the undo action */ - coroutineScope.launch { state.animateTo(RevealValue.Covered) } - }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Undo") }, - ) - }, - ) { - Chip( - modifier = - Modifier.fillMaxWidth().semantics { - // Use custom actions to make the primary and secondary actions accessible - customActions = - listOf( - CustomAccessibilityAction("Delete") { - /* Add the primary action click handler */ - true - } - ) - }, - onClick = { /* the click action associated with chip */ }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Swipe Me") }, - ) - } -} - -/** - * A sample on how to use Swipe To Reveal within a list of items, preferably [ScalingLazyColumn]. - */ -@Sampled -@Composable -fun SwipeToRevealWithExpandables() { - // Shape of actions should match with the overlay content. For example, Chips - // should use RoundedCornerShape(CornerSize(percent = 50)), Cards should use - // RoundedCornerShape with appropriate radius, based on the theme. - val actionShape = RoundedCornerShape(corner = CornerSize(percent = 50)) - val itemCount = 10 - val coroutineScope = rememberCoroutineScope() - val expandableStates = List(itemCount) { rememberExpandableState(initiallyExpanded = true) } - ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { - item { ListHeader { Text("Scaling Lazy Column") } } - repeat(itemCount) { current -> - expandableItem(state = expandableStates[current]) { isExpanded -> - val revealState = rememberRevealState() - if (isExpanded) { - SwipeToReveal( - state = revealState, - primaryAction = { - Box( - modifier = - Modifier.fillMaxSize() - .background(Color.Red, actionShape) - .clickable { - coroutineScope.launch { - revealState.animateTo(RevealValue.RightRevealed) - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "Delete", - ) - } - }, - secondaryAction = { - Box( - modifier = - Modifier.fillMaxSize() - .background(Color.Gray, actionShape) - .clickable { /* trigger the optional action */ }, - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "More Options", - ) - } - }, - undoAction = { - Chip( - modifier = Modifier.fillMaxWidth(), - onClick = { - coroutineScope.launch { - revealState.animateTo(RevealValue.Covered) - } - }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Undo") }, - ) - }, - onFullSwipe = { - coroutineScope.launch { - delay(1000) - expandableStates[current].expanded = false - } - }, - ) { - Chip( - modifier = - Modifier.fillMaxWidth().semantics { - // Use custom actions to make the primary and secondary actions - // accessible - customActions = - listOf( - CustomAccessibilityAction("Delete") { - /* Add the primary action click handler */ - coroutineScope.launch { - revealState.animateTo(RevealValue.RightRevealed) - } - true - }, - CustomAccessibilityAction("More Options") { - /* Add the secondary action click handler */ - true - }, - ) - }, - onClick = { /* the click action associated with chip */ }, - colors = ChipDefaults.secondaryChipColors(), - label = { Text(text = "Swipe Me") }, - ) - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt deleted file mode 100644 index 87e92be..0000000 --- a/wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt +++ /dev/null @@ -1,335 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.foundation.samples - -import androidx.annotation.Sampled -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.LocalPinnableContainer -import androidx.compose.ui.layout.PinnableContainer -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastFirstOrNull -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.TransformingLazyColumnDefaults -import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider -import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider.ItemEdge -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults -import androidx.wear.compose.material.Text -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.CardDefaults -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.TitleCard -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight -import kotlin.math.abs -import kotlinx.coroutines.launch - -@Sampled -@Preview -@Composable -fun SimpleTransformingLazyColumnSample() { - val transformationSpec = rememberTransformationSpec() - TransformingLazyColumn(contentPadding = PaddingValues(20.dp)) { - items(count = 10) { index -> - Button( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text(text = "Item $index") - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnWithSnapSample() { - val transformationSpec = rememberTransformationSpec() - val state = rememberTransformingLazyColumnState() - TransformingLazyColumn( - rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(scrollableState = state), - flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(state = state), - modifier = Modifier.fillMaxWidth(), - state = state, - contentPadding = PaddingValues(20.dp), - ) { - items(count = 20) { index -> - Button( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text(text = "Item $index") - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnAnimateItemSample() { - val state = rememberTransformingLazyColumnState() - - var list by remember { mutableStateOf(listOf("1", "2", "3")) } - - var next by remember { mutableIntStateOf(4) } - - Box(Modifier.fillMaxSize()) { - TransformingLazyColumn( - state = state, - contentPadding = PaddingValues(5.dp), - modifier = Modifier.background(Color.Black).fillMaxSize(), - ) { - items(list.size, key = { list[it] }) { - Text( - "Item ${list[it]}", - Modifier.animateItem().clickable { - list = list.filter { elem -> elem != list[it] } - }, - ) - } - } - Text( - "+", - Modifier.align(Alignment.CenterStart).padding(horizontal = 5.dp).clickable { - if (list.size < 25) list = list + "${next++}" - }, - ) - Text( - "S", - Modifier.align(Alignment.CenterEnd).padding(horizontal = 5.dp).clickable { - list = list.shuffled() - }, - ) - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnScrollToItemSample() { - val state = - rememberTransformingLazyColumnState( - // Customize initial scroll position of the TransformingLazyColumn. - initialAnchorItemIndex = 10 - ) - val coroutineScope = rememberCoroutineScope() - - TransformingLazyColumn( - modifier = Modifier.background(Color.Black), - state = state, - contentPadding = PaddingValues(vertical = 20.dp), - ) { - items(count = 20) { - Text( - "Item $it", - modifier = - Modifier.drawBehind { - val isCentered = - it == state.anchorItemIndex && - abs(state.anchorItemScrollOffset) < size.height - drawRect(if (isCentered) Color.Green else Color.DarkGray) - } - .padding(5.dp) - .clickable { coroutineScope.launch { state.scrollToItem(it) } }, - ) - } - - item { - Text( - "Scroll to top", - modifier = - Modifier.clickable { coroutineScope.launch { state.animateScrollToItem(0) } }, - ) - } - } - - LaunchedEffect(Unit) { - snapshotFlow { state.anchorItemIndex }.collect { println("Anchor item index: $it") } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnMinimumVerticalContentPaddingSample() { - val transformationSpec = rememberTransformationSpec() - - TransformingLazyColumn(contentPadding = PaddingValues(horizontal = 20.dp)) { - items(count = 20) { index -> - Button( - modifier = - Modifier.fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text(text = "Item $index") - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnFirstLayoutItemProviderSample() { - val state = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - var expandedItemIndex by remember { mutableIntStateOf(-1) } - - // This sample demonstrates how to use the provider API to control the direction of content - // shifting. By default, TransformingLazyColumn uses the center item as the layout reference. - // This means that if an item above the center expands, it pushes content upwards; - // if below, it pushes downwards. - // - // Here, we fix the Bottom/End edge of the clicked item regardless of its position on screen, - // so that when its animated content appears, the card predictably expands *upwards* every time. - val upwardExpandingItemProvider = - remember(state) { - TransformingLazyColumnFirstLayoutItemProvider { centerItem -> - val item = expandedItemIndex - - // Yield to the standard layout behavior during active scrolls. - // This avoids custom layout overhead and ensures the [TransformingLazyColumn] - // tracks the user's scroll gesture using its default center layout reference. - if (item == -1 || state.isScrollInProgress) { - return@TransformingLazyColumnFirstLayoutItemProvider centerItem - } - - // Look up the item's offset from state.layoutInfo (which holds the details - // from the previous measure pass) to maintain its visual position in the current - // pass. - state.layoutInfo.visibleItems - .fastFirstOrNull { visibleItem -> visibleItem.index == item } - ?.let { visibleItem -> - TransformingLazyColumnFirstLayoutItemProvider.ItemInfo( - key = visibleItem.key, - index = visibleItem.index, - // Pin the bottom edge of the item - itemEdge = ItemEdge.End, - // Calculate the exact bottom offset from the previous pass - offset = visibleItem.offset + visibleItem.transformedHeight, - ) - } ?: centerItem - } - } - - TransformingLazyColumn( - state = state, - contentPadding = PaddingValues(horizontal = 20.dp), - firstLayoutItemProvider = upwardExpandingItemProvider, - ) { - items(count = 10, key = { it }) { cardIndex -> - val isExpanded = expandedItemIndex == cardIndex - TitleCard( - onClick = { expandedItemIndex = cardIndex }, - modifier = - Modifier.minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ) - .fillMaxWidth() - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - title = { Text("Card $cardIndex") }, - subtitle = { - AnimatedVisibility(isExpanded) { Text("Expanded content is available here") } - }, - content = { Text("Tap to expand") }, - ) - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnPinnableContainerSample() { - val state = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - - TransformingLazyColumn(state = state, contentPadding = PaddingValues(horizontal = 20.dp)) { - items(count = 20) { index -> - var isPinned by remember { mutableStateOf(false) } - var pinHandle by remember { mutableStateOf(null) } - val pinnableContainer = LocalPinnableContainer.current - - // This state will be reset if the item is scrolled out and not pinned - var counter by remember { mutableIntStateOf(0) } - - TitleCard( - onClick = { counter++ }, - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - title = { Text("Item $index") }, - subtitle = { Text("Count: $counter (Tap card to +)") }, - content = { - // This button toggles the pin state - Button( - onClick = { - if (isPinned) { - pinHandle?.release().also { pinHandle = null } - isPinned = false - } else { - pinHandle = pinnableContainer?.pin() - isPinned = true - } - }, - colors = - if (isPinned) { - ButtonDefaults.buttonColors() - } else { - ButtonDefaults.filledTonalButtonColors() - }, - ) { - Text(if (isPinned) "Unpin" else "Pin Off-screen") - } - }, - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt deleted file mode 100644 index 79036c1..0000000 --- a/wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt +++ /dev/null @@ -1,377 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.AccountCircle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.AlertDialog -import androidx.wear.compose.material3.AlertDialogDefaults -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.SwitchButton -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Sampled -@Composable -@Preview -fun AlertDialogWithConfirmAndDismissSample() { - var showDialog by remember { mutableStateOf(false) } - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - icon = { - Icon( - Icons.Rounded.AccountCircle, - modifier = Modifier.size(32.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("Enable Battery Saver Mode?") }, - text = { Text("Your battery is low. Turn on battery saver.") }, - confirmButton = { - AlertDialogDefaults.ConfirmButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) - }, - dismissButton = { - AlertDialogDefaults.DismissButton( - onClick = { - // Perform dismiss action here - showDialog = false - } - ) - }, - ) { - item { Text(text = "You can configure battery saver mode in the setting menu.") } - item { Button(onClick = {}) { Text(text = "Go to settings") } } - } -} - -@Preview -@Sampled -@Composable -fun AlertDialogWithConfirmAndDismissTransformingContentSample() { - var showDialog by remember { mutableStateOf(false) } - val transformationSpec = rememberTransformationSpec() - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - icon = { - Icon( - Icons.Rounded.AccountCircle, - modifier = Modifier.size(32.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("Enable Battery Saver Mode?") }, - text = { Text("Your battery is low. Turn on battery saver.") }, - transformationSpec = transformationSpec, - confirmButton = { - AlertDialogDefaults.ConfirmButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) - }, - dismissButton = { - AlertDialogDefaults.DismissButton( - onClick = { - // Perform dismiss action here - showDialog = false - } - ) - }, - ) { - item { - Text( - modifier = - Modifier.transformedHeight(this, transformationSpec).graphicsLayer { - with(SurfaceTransformation(transformationSpec)) { - applyContentTransformation() - applyContainerTransformation() - } - }, - text = "You can configure battery saver mode in the setting menu.", - ) - } - item { - Button( - modifier = Modifier.transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text(text = "Go to settings") - } - } - } -} - -@Preview -@Sampled -@Composable -fun AlertDialogWithEdgeButtonSample() { - var showDialog by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - icon = { - Icon( - Icons.Rounded.AccountCircle, - modifier = Modifier.size(32.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("Mobile network is not currently available") }, - text = { Text("Please try again later or check your network settings.") }, - edgeButton = { - AlertDialogDefaults.EdgeButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) - }, - ) -} - -@Preview -@Sampled -@Composable -fun AlertDialogWithEdgeButtonTransformingContentSample() { - var showDialog by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - icon = { - Icon( - Icons.Rounded.AccountCircle, - modifier = Modifier.size(32.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("Mobile network is not currently available") }, - text = { Text("Please try again later or check your network settings.") }, - transformationSpec = rememberTransformationSpec(), - edgeButton = { - AlertDialogDefaults.EdgeButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) - }, - ) -} - -@Preview -@Sampled -@Composable -fun AlertDialogWithContentGroupsSample() { - var showDialog by remember { mutableStateOf(false) } - var weatherEnabled by remember { mutableStateOf(false) } - var calendarEnabled by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - title = { Text("Share your location") }, - text = { Text(" The following apps have asked you to share your location") }, - edgeButton = { - AlertDialogDefaults.EdgeButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) { - Text("Share once") - } - }, - ) { - item { - SwitchButton( - modifier = Modifier.fillMaxWidth(), - checked = weatherEnabled, - onCheckedChange = { weatherEnabled = it }, - label = { Text("Weather") }, - ) - } - item { - SwitchButton( - modifier = Modifier.fillMaxWidth(), - checked = calendarEnabled, - onCheckedChange = { calendarEnabled = it }, - label = { Text("Calendar") }, - ) - } - item { AlertDialogDefaults.GroupSeparator() } - item { - FilledTonalButton( - modifier = Modifier.fillMaxWidth(), - onClick = {}, - label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") }, - ) - } - item { - FilledTonalButton( - modifier = Modifier.fillMaxWidth(), - onClick = {}, - label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") }, - ) - } - } -} - -@Preview -@Sampled -@Composable -fun AlertDialogWithContentGroupsTransformingContentSample() { - var showDialog by remember { mutableStateOf(false) } - var weatherEnabled by remember { mutableStateOf(false) } - var calendarEnabled by remember { mutableStateOf(false) } - val transformationSpec = rememberTransformationSpec() - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showDialog = true }, - label = { Text("Show Dialog") }, - ) - } - AlertDialog( - visible = showDialog, - onDismissRequest = { showDialog = false }, - title = { Text("Share your location") }, - text = { Text(" The following apps have asked you to share your location") }, - transformationSpec = transformationSpec, - edgeButton = { - AlertDialogDefaults.EdgeButton( - onClick = { - // Perform confirm action here - showDialog = false - } - ) { - Text("Share once") - } - }, - ) { - item { - SwitchButton( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - checked = weatherEnabled, - onCheckedChange = { weatherEnabled = it }, - label = { Text("Weather") }, - transformation = SurfaceTransformation(transformationSpec), - ) - } - item { - SwitchButton( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - checked = calendarEnabled, - onCheckedChange = { calendarEnabled = it }, - label = { Text("Calendar") }, - transformation = SurfaceTransformation(transformationSpec), - ) - } - item { AlertDialogDefaults.GroupSeparator() } - item { - FilledTonalButton( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - onClick = {}, - label = { Text(modifier = Modifier.fillMaxWidth(), text = "Never share") }, - transformation = SurfaceTransformation(transformationSpec), - ) - } - item { - FilledTonalButton( - modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), - onClick = {}, - label = { Text(modifier = Modifier.fillMaxWidth(), text = "Share always") }, - transformation = SurfaceTransformation(transformationSpec), - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt b/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt deleted file mode 100644 index 4cd8d4a..0000000 --- a/wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt +++ /dev/null @@ -1,165 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontVariation -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.wear.compose.material3.AnimatedText -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.rememberAnimatedTextFontRegistry -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun AnimatedTextSample() { - val scope = rememberCoroutineScope() - val animatable = remember { Animatable(0f) } - val animate = { - scope.launch { - // Animate from 0 to 1 and then back to 0. - animatable.animateTo(1f) - animatable.animateTo(0f) - } - } - val animatedTextFontRegistry = - rememberAnimatedTextFontRegistry( - // Variation axes at the start of the animation, width 10, weight 200 - startFontVariationSettings = - FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)), - // Variation axes at the end of the animation, width 100, weight 500 - endFontVariationSettings = - FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)), - startFontSize = 30.sp, - endFontSize = 40.sp, - ) - AnimatedText( - text = "Hello!", - fontRegistry = animatedTextFontRegistry, - // Content alignment anchors the animation at the vertical center, expanding horizontally - contentAlignment = Alignment.CenterStart, - progressFraction = { animatable.value }, - modifier = Modifier.clickable(onClick = { animate() }), - ) - LaunchedEffect(Unit) { animate() } -} - -@Sampled -@Composable -fun AnimatedTextSampleButtonResponse() { - val scope = rememberCoroutineScope() - val animatedTextFontRegistry = - rememberAnimatedTextFontRegistry( - // Variation axes at the start of the animation, width 10, weight 200 - startFontVariationSettings = - FontVariation.Settings(FontVariation.width(10f), FontVariation.weight(200)), - // Variation axes at the end of the animation, width 100, weight 500 - endFontVariationSettings = - FontVariation.Settings(FontVariation.width(100f), FontVariation.weight(500)), - startFontSize = 30.sp, - endFontSize = 30.sp, - ) - val number = remember { mutableIntStateOf(0) } - val textAnimatable = remember { Animatable(0f) } - Row(verticalAlignment = Alignment.CenterVertically) { - Button( - modifier = Modifier.padding(horizontal = 16.dp), - onClick = { - number.value -= 1 - scope.launch { - textAnimatable.animateTo(1f) - textAnimatable.animateTo(0f) - } - }, - label = { - Text(modifier = Modifier.semantics { contentDescription = "Decrease" }, text = "-") - }, - ) - AnimatedText( - text = "${number.value}", - fontRegistry = animatedTextFontRegistry, - progressFraction = { textAnimatable.value }, - ) - Button( - modifier = Modifier.padding(horizontal = 16.dp), - onClick = { - number.value += 1 - scope.launch { - textAnimatable.animateTo(1f) - textAnimatable.animateTo(0f) - } - }, - label = { - Text(modifier = Modifier.semantics { contentDescription = "Increase" }, text = "+") - }, - ) - } -} - -@Sampled -@Composable -fun AnimatedTextSampleSharedFontRegistry() { - val animatedTextFontRegistry = - rememberAnimatedTextFontRegistry( - // Variation axes at the start of the animation, width 50, weight 300 - startFontVariationSettings = - FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)), - // Variation axes at the end of the animation are the same as the start axes - endFontVariationSettings = - FontVariation.Settings(FontVariation.width(50f), FontVariation.weight(300)), - startFontSize = 15.sp, - endFontSize = 25.sp, - ) - val firstAnimatable = remember { Animatable(0f) } - val secondAnimatable = remember { Animatable(0f) } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - AnimatedText( - text = "Top Text", - fontRegistry = animatedTextFontRegistry, - progressFraction = { firstAnimatable.value }, - ) - AnimatedText( - text = "Bottom Text", - fontRegistry = animatedTextFontRegistry, - progressFraction = { secondAnimatable.value }, - ) - } - LaunchedEffect(Unit) { - firstAnimatable.animateTo(1f) - firstAnimatable.animateTo(0f) - secondAnimatable.animateTo(1f) - secondAnimatable.animateTo(0f) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt deleted file mode 100644 index b29826a..0000000 --- a/wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt +++ /dev/null @@ -1,91 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonGroup -import androidx.wear.compose.material3.Text - -@Sampled -@Composable -fun ButtonGroupSample() { - val interactionSource1 = remember { MutableInteractionSource() } - val interactionSource2 = remember { MutableInteractionSource() } - - Box(contentAlignment = Alignment.Center) { - ButtonGroup(Modifier.fillMaxWidth()) { - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource1), - interactionSource = interactionSource1, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("L") } - } - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource2), - interactionSource = interactionSource2, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("R") } - } - } - } -} - -@Sampled -@Composable -fun ButtonGroupThreeButtonsSample() { - val interactionSource1 = remember { MutableInteractionSource() } - val interactionSource2 = remember { MutableInteractionSource() } - val interactionSource3 = remember { MutableInteractionSource() } - - Box(contentAlignment = Alignment.Center) { - ButtonGroup(Modifier.fillMaxWidth()) { - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource1), - interactionSource = interactionSource1, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("A") } - } - Button( - onClick = {}, - modifier = Modifier.weight(1.5f).animateWidth(interactionSource2), - interactionSource = interactionSource2, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("B") } - } - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource3), - interactionSource = interactionSource3, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Text("C") } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt deleted file mode 100644 index 5a3c305..0000000 --- a/wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt +++ /dev/null @@ -1,496 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.sp -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.ChildButton -import androidx.wear.compose.material3.CompactButton -import androidx.wear.compose.material3.CompactButtonDefaults -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.OutlinedButton -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState -import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture -import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun SimpleButtonSample(modifier: Modifier = Modifier) { - Button(onClick = { /* Do something */ }, label = { Text("Simple Button") }, modifier = modifier) -} - -@Sampled -@Composable -fun ButtonSample() { - Button( - onClick = { /* Do something */ }, - label = { Text("Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun ButtonLargeIconSample(enabled: Boolean = true) { - // When customising the icon size, it is recommended to also specify - // the associated content padding - Button( - onClick = { /* Do something */ }, - enabled = enabled, - label = { Text("Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.LargeIconSize), - ) - }, - contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun ButtonExtraLargeIconSample(enabled: Boolean = true) { - // When customising the icon size, it is recommended to also specify - // the associated content padding - Button( - onClick = { /* Do something */ }, - enabled = enabled, - label = { Text("Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.ExtraLargeIconSize), - ) - }, - contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun ButtonWithImageSample(enabled: Boolean = true) { - Button( - onClick = { /* Do something */ }, - containerPainter = - ButtonDefaults.containerPainter( - image = painterResource(id = R.drawable.backgroundimage) - ), - enabled = enabled, - label = { Text("Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun SimpleFilledTonalButtonSample() { - FilledTonalButton( - onClick = { /* Do something */ }, - label = { Text("Filled Tonal Button") }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun FilledTonalButtonSample() { - FilledTonalButton( - onClick = { /* Do something */ }, - label = { Text("Filled Tonal Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun SimpleFilledVariantButtonSample() { - Button( - onClick = { /* Do something */ }, - colors = ButtonDefaults.filledVariantButtonColors(), - label = { Text("Filled Variant Button") }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun FilledVariantButtonSample() { - Button( - onClick = { /* Do something */ }, - colors = ButtonDefaults.filledVariantButtonColors(), - label = { Text("Filled Variant Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun SimpleOutlinedButtonSample() { - OutlinedButton( - onClick = { /* Do something */ }, - label = { Text("Outlined Button") }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun OutlinedButtonSample() { - OutlinedButton( - onClick = { /* Do something */ }, - label = { Text("Outlined Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun SimpleChildButtonSample() { - ChildButton( - onClick = { /* Do something */ }, - label = { - Text("Child Button", textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun ChildButtonSample() { - ChildButton( - onClick = { /* Do something */ }, - label = { Text("Child Button") }, - secondaryLabel = { Text("Secondary label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Sampled -@Composable -fun CompactButtonSample(modifier: Modifier = Modifier) { - CompactButton( - onClick = { /* Do something */ }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), - ) - }, - modifier = modifier, - label = { Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis) }, - ) -} - -@Sampled -@Composable -fun CompactButtonWithContentSample(modifier: Modifier = Modifier) { - CompactButton( - onClick = { /* Do something */ }, - modifier = modifier, - content = { - Box( - Modifier.size(CompactButtonDefaults.ExtraSmallIconSize) - .clip(CircleShape) - .background(Color.Green) - ) - Spacer(Modifier.width(ButtonDefaults.IconSpacing)) - Text( - "Custom content", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - fontStyle = FontStyle.Italic, - letterSpacing = 0.5.sp, - ) - }, - ) -} - -@Sampled -@Composable -fun CompactButtonWithOnLongClickSample( - onClickHandler: () -> Unit, - onLongClickHandler: () -> Unit, - modifier: Modifier = Modifier, -) { - CompactButton( - onClick = onClickHandler, - onLongClick = onLongClickHandler, - onLongClickLabel = "Long click", - label = { Text("Long clickable") }, - modifier = - modifier.semantics { - // Also override the 'click label' to say 'Double tap to press' instead of - // the usual 'Double tap to activate'. - onClick("press") { false } - }, - ) -} - -@Sampled -@Composable -fun FilledTonalCompactButtonSample(modifier: Modifier = Modifier) { - CompactButton( - onClick = { /* Do something */ }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), - ) - }, - colors = ButtonDefaults.filledTonalButtonColors(), - modifier = modifier, - label = { - Text("Filled Tonal Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis) - }, - ) -} - -@Sampled -@Composable -fun OutlinedCompactButtonSample(modifier: Modifier = Modifier) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - var expanded by remember { mutableStateOf(false) } - if (expanded) { - Text("A multiline string showing two lines") - } else { - Text("One line text") - } - Spacer(Modifier.height(ButtonDefaults.IconSpacing)) - CompactButton( - onClick = { expanded = !expanded }, - colors = ButtonDefaults.outlinedButtonColors(), - border = ButtonDefaults.outlinedButtonBorder(enabled = true), - modifier = modifier, - content = { - if (expanded) { - Text("Show Less", maxLines = 1, overflow = TextOverflow.Ellipsis) - } else { - Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis) - } - Spacer(Modifier.width(ButtonDefaults.IconSpacing)) - if (expanded) { - Icon( - Icons.Filled.KeyboardArrowUp, - contentDescription = "Collapse", - modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), - ) - } else { - Icon( - Icons.Filled.KeyboardArrowDown, - contentDescription = "Expand", - modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), - ) - } - }, - ) - } -} - -@Sampled -@Composable -fun ButtonContentWithOneHandedGestureSample() { - var label by remember { mutableStateOf("Filled Button") } - val onClick = remember { { label = "Gestured" } } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "click", - interactionSource = interactionSource, - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - onGesture = onClick, - ), - content = { - OneHandedGestureClickIndicator( - gestureConfiguration = gestureConfig, - state = indicatorState, - gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary, - ) { - ButtonDefaults.Content( - secondaryLabel = { Text("Secondary Label") }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - colors = ButtonDefaults.buttonColors(), - label = { Text(label) }, - ) - } - }, - ) - } -} - -@Sampled -@Composable -fun CompactButtonContentWithOneHandedGestureSample() { - var label by remember { mutableStateOf("Compact Button") } - val onClick = remember { { label = "Gestured" } } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CompactButton( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "click", - interactionSource = interactionSource, - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - onGesture = onClick, - ), - content = { - OneHandedGestureClickIndicator( - gestureConfiguration = gestureConfig, - state = indicatorState, - gestureIndicatorTint = MaterialTheme.colorScheme.onPrimary, - ) { - CompactButtonDefaults.Content( - icon = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(CompactButtonDefaults.ExtraSmallIconSize), - ) - }, - colors = ButtonDefaults.buttonColors(), - label = { Text(label) }, - ) - } - }, - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt b/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt deleted file mode 100644 index bcf5d47..0000000 --- a/wear/wear-compose-m3/references/material3/CardSample.kt.md.txt +++ /dev/null @@ -1,470 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.AppCard -import androidx.wear.compose.material3.Card -import androidx.wear.compose.material3.CardDefaults -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.OutlinedCard -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TitleCard -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState -import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture -import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration -import kotlinx.coroutines.launch - -@Preview -@Sampled -@Composable -fun CardSample() { - Card(onClick = { /* Do something */ }) { Text("Card") } -} - -@Sampled -@Composable -fun NonClickableCardSample() { - Card { Text("Non Clickable Card") } -} - -@Sampled -@Composable -fun CardWithOnLongClickSample(onLongClickHandler: () -> Unit) { - Card( - onClick = { /* Do something */ }, - onLongClick = onLongClickHandler, - onLongClickLabel = "Long click", - ) { - Text("Card with long click") - } -} - -@Sampled -@Composable -fun AppCardSample() { - AppCard( - onClick = { /* Do something */ }, - appName = { Text("App name") }, - title = { Text("Card title") }, - time = { Text("Now") }, - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun NonClickableAppCardSample() { - AppCard( - appName = { Text("App name") }, - title = { Text("Card title") }, - time = { Text("Now") }, - ) { - Text("Non clickable card content") - } -} - -@Sampled -@Composable -fun AppCardWithIconSample() { - AppCard( - onClick = { /* Do something */ }, - appName = { Text("App name") }, - appImage = { - Icon( - painter = painterResource(id = android.R.drawable.star_big_off), - contentDescription = "Star icon", - modifier = - Modifier.size(CardDefaults.AppImageSize) - .wrapContentSize(align = Alignment.Center), - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("Card title") }, - time = { Text("Now") }, - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun AppCardWithImageSample() { - val configuration = LocalConfiguration.current - // Add padding to the end of the image in order to maintain the correct proportions - // between the image and the card. - val imageEndPaddingDp = (0.15f * configuration.screenWidthDp).dp - AppCard( - onClick = { /* Do something */ }, - appName = { Text("App name") }, - appImage = { - Icon( - painter = painterResource(id = android.R.drawable.star_big_off), - contentDescription = "Star icon", - modifier = - Modifier.size(CardDefaults.AppImageSize) - .wrapContentSize(align = Alignment.Center), - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { Text("With image") }, - time = { Text("Now") }, - ) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Image( - modifier = - Modifier.weight(1f).aspectRatio(16f / 9f).clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = R.drawable.card_content_image), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - Spacer(modifier = Modifier.width(imageEndPaddingDp)) - } - } -} - -@Sampled -@Composable -fun TitleCardSample() { - TitleCard( - onClick = { /* Do something */ }, - title = { Text("Title card") }, - time = { Text("Now") }, - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun NonClickableTitleCardSample() { - TitleCard(title = { Text("Title card") }, time = { Text("Now") }) { - Text("Non clickable Card content") - } -} - -@Sampled -@Composable -fun TitleCardWithSubtitleAndTimeSample() { - TitleCard( - onClick = { /* Do something */ }, - time = { Text("Now") }, - title = { Text("Title card") }, - subtitle = { Text("Subtitle") }, - ) -} - -@Preview -@Sampled -@Composable -fun TitleCardWithMultipleImagesSample() { - TitleCard( - onClick = { /* Do something */ }, - title = { Text("Title card") }, - time = { Text("Now") }, - modifier = Modifier.semantics { contentDescription = "Background image" }, - ) { - Spacer(Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Image( - modifier = - Modifier.weight(2f) - .height(68.dp) - .align(Alignment.CenterVertically) - .clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = R.drawable.card_content_image), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - Spacer(Modifier.width(4.dp)) - Image( - modifier = - Modifier.weight(1f) - .height(68.dp) - .align(Alignment.CenterVertically) - .clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = R.drawable.card_content_image), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - } - } -} - -@Sampled -@Composable -fun TitleCardWithImageWithTimeAndTitleSample() { - TitleCard( - onClick = { /* Do something */ }, - containerPainter = - CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), - title = { Text("Card title") }, - subtitle = { Text("Subtitle") }, - time = { Text("Now") }, - contentPadding = CardDefaults.CardWithContainerPainterContentPadding, - modifier = Modifier.semantics { contentDescription = "Background image" }, - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun NonClickableTitleCardWithImageWithTimeAndTitleSample() { - TitleCard( - containerPainter = - CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), - title = { Text("Card title") }, - subtitle = { Text("Subtitle") }, - time = { Text("Now") }, - contentPadding = CardDefaults.CardWithContainerPainterContentPadding, - modifier = Modifier.semantics { contentDescription = "Background image" }, - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun OutlinedCardSample() { - OutlinedCard(onClick = { /* Do something */ }) { Text("Outlined card") } -} - -@Sampled -@Composable -fun NonClickableOutlinedCardSample() { - OutlinedCard { Text("Non-clickable outlined card") } -} - -@Sampled -@Composable -fun ImageCardSample() { - Card( - onClick = { /* Do something */ }, - containerPainter = - CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)), - ) { - Text("Image card") - } -} - -@Sampled -@Composable -fun NonClickableImageCardSample() { - Card( - containerPainter = - CardDefaults.containerPainter(image = painterResource(id = R.drawable.backgroundimage)) - ) { - Text("Non clickable image card") - } -} - -@Sampled -@Composable -fun OutlinedAppCardSample() { - AppCard( - onClick = { /* Do something */ }, - appName = { Text("App name") }, - appImage = { - Icon( - Icons.Filled.Favorite, - contentDescription = "Favorite icon", - modifier = Modifier.size(CardDefaults.AppImageSize), - ) - }, - title = { Text("App card") }, - time = { Text("Now") }, - colors = CardDefaults.outlinedCardColors(), - border = CardDefaults.outlinedCardBorder(), - ) { - Text("Card content") - } -} - -@Sampled -@Composable -fun OutlinedTitleCardSample() { - TitleCard( - onClick = { /* Do something */ }, - title = { Text("Title card") }, - time = { Text("Now") }, - colors = CardDefaults.outlinedCardColors(), - border = CardDefaults.outlinedCardBorder(), - ) { - Text("Card content") - } -} - -@Sampled -@Preview -@Composable -fun CardFillContentSample() { - Card( - onClick = { /* Do something */ }, - // Constrains the card to fill background up to the intrinsic height. - modifier = Modifier.height(IntrinsicSize.Min), - ) { - Text( - "Filled Content", - color = MaterialTheme.colorScheme.onPrimary, - modifier = - Modifier.fillMaxHeight() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primary) - .wrapContentSize(Alignment.Center), - ) - } -} - -@Sampled -@Composable -fun AppCardContentWithOneHandedGestureSample() { - var label by remember { mutableStateOf("App Card") } - val onClick = remember { { label = "Gestured" } } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Card( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.padding(horizontal = 12.dp) - .fillMaxWidth() - .oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "click", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - interactionSource = interactionSource, - onGesture = onClick, - ), - ) { - OneHandedGestureClickIndicator( - gestureConfiguration = gestureConfig, - state = indicatorState, - gestureIndicatorTint = MaterialTheme.colorScheme.onSurface, - ) { - CardDefaults.AppCardContent( - appName = { Text("App Name") }, - title = { Text(label) }, - appImage = { - Icon( - painter = painterResource(R.drawable.ic_favorite_rounded), - contentDescription = "Favorite icon", - modifier = Modifier.size(CardDefaults.AppImageSize), - ) - }, - time = { Text("now") }, - ) { - Text("Card body") - } - } - } - } -} - -@Sampled -@Composable -fun TitleCardContentWithOneHandedGestureSample() { - var label by remember { mutableStateOf("Title Card") } - val onClick = remember { { label = "Gestured" } } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Card( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.padding(horizontal = 12.dp) - .fillMaxWidth() - .oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "click", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - interactionSource = interactionSource, - onGesture = onClick, - ), - ) { - OneHandedGestureClickIndicator( - gestureConfiguration = gestureConfig, - state = indicatorState, - gestureIndicatorTint = MaterialTheme.colorScheme.onSurface, - ) { - CardDefaults.TitleCardContent( - title = { Text(label) }, - time = { Text("now") }, - subtitle = { Text("Subtitle") }, - ) { - Text("Card body") - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt deleted file mode 100644 index 3ae73bd..0000000 --- a/wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt +++ /dev/null @@ -1,69 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.material3.CheckboxButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.SplitCheckboxButton -import androidx.wear.compose.material3.Text - -@Sampled -@Preview -@Composable -fun CheckboxButtonSample() { - var checked by remember { mutableStateOf(true) } - CheckboxButton( - label = { Text("Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - secondaryLabel = { - Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) - }, - checked = checked, - onCheckedChange = { checked = it }, - icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, - enabled = true, - ) -} - -@Sampled -@Preview -@Composable -fun SplitCheckboxButtonSample() { - var checked by remember { mutableStateOf(true) } - SplitCheckboxButton( - label = { Text("Split Checkbox Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - checked = checked, - onCheckedChange = { checked = it }, - toggleContentDescription = "Split Checkbox Button Sample", - onContainerClick = { - /* Do something */ - }, - containerClickLabel = "click", - enabled = true, - ) -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt deleted file mode 100644 index 1f31892..0000000 --- a/wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt +++ /dev/null @@ -1,162 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.wear.compose.material3.ConfirmationDialog -import androidx.wear.compose.material3.ConfirmationDialogDefaults -import androidx.wear.compose.material3.FailureConfirmationDialog -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.SuccessConfirmationDialog -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.confirmationDialogCurvedText -import androidx.wear.compose.material3.samples.icons.FavoriteIcon - -@Sampled -@Composable -fun ConfirmationDialogSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Show Confirmation") }, - ) - } - - // Has an icon and a short curved text content, which will be displayed along the bottom edge of - // the screen. - val curvedTextStyle = ConfirmationDialogDefaults.curvedTextStyle - ConfirmationDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - curvedText = { confirmationDialogCurvedText("Confirmed", curvedTextStyle) }, - ) { - FavoriteIcon(ConfirmationDialogDefaults.IconSize) - } -} - -@Sampled -@Composable -fun LongTextConfirmationDialogSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Show Confirmation") }, - ) - } - - // Has an icon and a text content. Text will be displayed in the center of the screen below the - // icon. - ConfirmationDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - text = { Text(text = "Your message has been sent") }, - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Send, - contentDescription = null, - modifier = Modifier.size(ConfirmationDialogDefaults.SmallIconSize), - ) - } -} - -@Sampled -@Composable -fun FailureConfirmationDialogSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Show Confirmation") }, - ) - } - - val text = "Failure" - val style = ConfirmationDialogDefaults.curvedTextStyle - FailureConfirmationDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - curvedText = { confirmationDialogCurvedText(text, style) }, - ) -} - -@Sampled -@Composable -fun FailureConfirmationDialogWithGenericFailureIconSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Show Confirmation") }, - ) - } - - val text = "Failure" - val style = ConfirmationDialogDefaults.curvedTextStyle - FailureConfirmationDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - curvedText = { confirmationDialogCurvedText(text, style) }, - content = { ConfirmationDialogDefaults.GenericFailureIcon() }, - ) -} - -@Sampled -@Composable -fun SuccessConfirmationDialogSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Show Confirmation") }, - ) - } - - val text = "Success" - val style = ConfirmationDialogDefaults.curvedTextStyle - SuccessConfirmationDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - curvedText = { confirmationDialogCurvedText(text, style) }, - ) -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt b/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt deleted file mode 100644 index 4e1a9ae..0000000 --- a/wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt +++ /dev/null @@ -1,78 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap.Companion.Round -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.CurvedDirection -import androidx.wear.compose.foundation.CurvedLayout -import androidx.wear.compose.foundation.CurvedModifier -import androidx.wear.compose.foundation.angularSizeDp -import androidx.wear.compose.foundation.background -import androidx.wear.compose.foundation.curvedBox -import androidx.wear.compose.foundation.curvedComposable -import androidx.wear.compose.foundation.curvedRow -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.CurvedTextDefaults -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.curvedText - -@Sampled -@Composable -fun CurvedTextTop() { - val backgroundColor = MaterialTheme.colorScheme.onPrimary - val customColor = MaterialTheme.colorScheme.tertiaryDim - CurvedLayout { - curvedRow(CurvedModifier.background(backgroundColor, Round)) { - curvedText("Calling", color = customColor) - curvedBox(CurvedModifier.angularSizeDp(5.dp)) {} - curvedText("Camilia Garcia") - } - } -} - -@Sampled -@Composable -fun CurvedTextBottom() { - val backgroundColor = MaterialTheme.colorScheme.onPrimary - CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) { - curvedRow(CurvedModifier.background(backgroundColor, Round)) { - curvedComposable { - Icon( - Icons.Filled.Warning, - contentDescription = "Warning", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - } - curvedText( - "Network lost", - maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle, - overflow = TextOverflow.Ellipsis, - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt b/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt deleted file mode 100644 index 261e5d7..0000000 --- a/wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt +++ /dev/null @@ -1,131 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.DatePicker -import androidx.wear.compose.material3.DatePickerType -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.Text -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Sampled -@Composable -fun DatePickerSample() { - var showDatePicker by remember { mutableStateOf(true) } - var datePickerDate by remember { mutableStateOf(LocalDate.now()) } - val formatter = - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(LocalConfiguration.current.locales[0]) - if (showDatePicker) { - DatePicker( - initialDate = datePickerDate, // Initialize with last picked date on reopen - onDatePicked = { - datePickerDate = it - showDatePicker = false - }, - ) - } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = { showDatePicker = true }, - label = { Text("Selected Date") }, - secondaryLabel = { Text(datePickerDate.format(formatter)) }, - icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, - ) - } - } -} - -@Sampled -@Composable -fun DatePickerYearMonthDaySample() { - var showDatePicker by remember { mutableStateOf(true) } - var datePickerDate by remember { mutableStateOf(LocalDate.now()) } - - val formatter = - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(LocalConfiguration.current.locales[0]) - - if (showDatePicker) { - DatePicker( - initialDate = datePickerDate, // Initialize with last picked date on reopen - onDatePicked = { - datePickerDate = it - showDatePicker = false - }, - datePickerType = DatePickerType.YearMonthDay, - ) - } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = { showDatePicker = true }, - label = { Text("Selected Date") }, - secondaryLabel = { Text(datePickerDate.format(formatter)) }, - icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, - ) - } - } -} - -@Sampled -@Composable -fun DatePickerFutureOnlySample() { - val currentDate = LocalDate.now() - var showDatePicker by remember { mutableStateOf(true) } - var datePickerDate by remember { mutableStateOf(LocalDate.now()) } - val formatter = - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(LocalConfiguration.current.locales[0]) - if (showDatePicker) { - DatePicker( - initialDate = datePickerDate, // Initialize with last picked date on reopen - onDatePicked = { - datePickerDate = it - showDatePicker = false - }, - datePickerType = DatePickerType.YearMonthDay, - minValidDate = currentDate, - ) - } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = { showDatePicker = true }, - label = { Text("Selected Date") }, - secondaryLabel = { Text(datePickerDate.format(formatter)) }, - icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") }, - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt b/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt deleted file mode 100644 index 45d4651..0000000 --- a/wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt +++ /dev/null @@ -1,74 +0,0 @@ -``` -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.ColorScheme -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.dynamicColorScheme -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Sampled -@Composable -fun DynamicColorSchemeSample() { - val dynamicColorScheme = dynamicColorScheme(LocalContext.current) - val transformationSpec = rememberTransformationSpec() - - // Fallback to the default color scheme if dynamic colors are unavailable - MaterialTheme(colorScheme = dynamicColorScheme ?: ColorScheme()) { - val hasDynamicColors = dynamicColorScheme != null - TransformingLazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp), - contentPadding = PaddingValues(20.dp), - ) { - if (!hasDynamicColors) { - item { Text("Dynamic color is not available.") } - } - items(5) { index -> - // The button's defaults will pick up the dynamic primary color from the - // MaterialTheme - Button( - label = { Text("Primary Button ${index + 1}") }, - modifier = - Modifier.fillMaxWidth() - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ), - onClick = {}, - transformation = SurfaceTransformation(transformationSpec), - ) - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt deleted file mode 100644 index 4a01bc8..0000000 --- a/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt +++ /dev/null @@ -1,63 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.EdgeButton -import androidx.wear.compose.material3.EdgeButtonSize -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.Text - -@Sampled -@Composable -fun EdgeButtonSample() { - Column( - Modifier.fillMaxSize().padding(horizontal = 20.dp).padding(top = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) { - Text( - "EdgeButton hugs the bottom of the curved screen", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - ) - } - EdgeButton(onClick = { /* Do something */ }, buttonSize = EdgeButtonSize.Medium) { - Icon( - Icons.Filled.Check, - contentDescription = "Check icon", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt b/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt deleted file mode 100644 index 4605ac0..0000000 --- a/wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt +++ /dev/null @@ -1,60 +0,0 @@ -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.FadingExpandingLabel - -@Sampled -@Composable -fun FadingExpandingLabelButtonSample() { - var text by remember { mutableStateOf("Line of Text One.") } - var lines by remember { mutableIntStateOf(1) } - - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = { - lines = lines % 3 + 1 - text = - (1..lines).joinToString(separator = "\n") { - when (it) { - 1 -> "Line of Text One." - 2 -> "Line of Text Two." - else -> "Line of Text Three." - } - } - }, - modifier = Modifier.fillMaxWidth(), - label = { FadingExpandingLabel(text = text, textAlign = TextAlign.Left) }, - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt deleted file mode 100644 index 7ecc104..0000000 --- a/wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt +++ /dev/null @@ -1,125 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.Image -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.wear.compose.material3.FilledIconButton -import androidx.wear.compose.material3.FilledTonalIconButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.IconButton -import androidx.wear.compose.material3.IconButtonColors -import androidx.wear.compose.material3.IconButtonDefaults -import androidx.wear.compose.material3.IconButtonShapes -import androidx.wear.compose.material3.OutlinedIconButton - -@Composable -@Sampled -fun IconButtonSample() { - IconButton(onClick = { /* Do something */ }) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun FilledIconButtonSample() { - FilledIconButton(onClick = { /* Do something */ }) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun FilledVariantIconButtonSample() { - FilledIconButton( - onClick = { /* Do something */ }, - colors = IconButtonDefaults.filledVariantIconButtonColors(), - ) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun FilledTonalIconButtonSample() { - FilledTonalIconButton(onClick = { /* Do something */ }) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun OutlinedIconButtonSample() { - OutlinedIconButton(onClick = { /* Do something */ }) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Sampled -@Composable -fun IconButtonWithOnLongClickSample(onLongClick: () -> Unit) { - IconButton( - onClick = { /* Do something for onClick*/ }, - onLongClick = onLongClick, - onLongClickLabel = "Long click", - ) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun IconButtonWithCornerAnimationSample( - colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors() -) { - FilledIconButton( - onClick = { /* Do something */ }, - shapes = IconButtonDefaults.animatedShapes(), - colors = colors, - ) { - Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon") - } -} - -@Composable -@Sampled -fun IconButtonWithImageSample( - painter: Painter = painterResource(R.drawable.card_content_image), - enabled: Boolean = true, - shapes: IconButtonShapes = IconButtonDefaults.shapes(), -) { - IconButton(onClick = { /* Do something */ }, shapes = shapes, enabled = enabled) { - Image( - painter = painter, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = - if (enabled) Modifier else Modifier.alpha(IconButtonDefaults.DisabledImageOpacity), - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt deleted file mode 100644 index 23c16da..0000000 --- a/wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt +++ /dev/null @@ -1,116 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.IconToggleButton -import androidx.wear.compose.material3.IconToggleButtonDefaults -import androidx.wear.compose.material3.samples.icons.WifiOffIcon -import androidx.wear.compose.material3.samples.icons.WifiOnIcon - -@Sampled -@Composable -fun IconToggleButtonSample() { - var firstChecked by remember { mutableStateOf(true) } - var secondChecked by remember { mutableStateOf(false) } - - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - IconToggleButton( - checked = firstChecked, - onCheckedChange = { firstChecked = !firstChecked }, - shapes = IconToggleButtonDefaults.animatedShapes(), - ) { - if (firstChecked) { - WifiOnIcon() - } else { - WifiOffIcon() - } - } - - Spacer(modifier = Modifier.width(5.dp)) - - IconToggleButton( - checked = secondChecked, - onCheckedChange = { secondChecked = !secondChecked }, - shapes = IconToggleButtonDefaults.animatedShapes(), - ) { - if (secondChecked) { - WifiOnIcon() - } else { - WifiOffIcon() - } - } - } -} - -@Sampled -@Composable -fun IconToggleButtonVariantSample() { - var firstChecked by remember { mutableStateOf(true) } - var secondChecked by remember { mutableStateOf(false) } - - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - IconToggleButton( - checked = firstChecked, - onCheckedChange = { firstChecked = !firstChecked }, - shapes = IconToggleButtonDefaults.variantAnimatedShapes(), - ) { - if (firstChecked) { - WifiOnIcon() - } else { - WifiOffIcon() - } - } - - Spacer(modifier = Modifier.width(5.dp)) - - IconToggleButton( - checked = secondChecked, - onCheckedChange = { secondChecked = !secondChecked }, - shapes = IconToggleButtonDefaults.variantAnimatedShapes(), - ) { - if (secondChecked) { - WifiOnIcon() - } else { - WifiOffIcon() - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt deleted file mode 100644 index 7f02be2..0000000 --- a/wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt +++ /dev/null @@ -1,68 +0,0 @@ -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.IconButton -import androidx.wear.compose.material3.LevelIndicator -import androidx.wear.compose.material3.StepperDefaults -import androidx.wear.compose.material3.samples.icons.VolumeDownIcon -import androidx.wear.compose.material3.samples.icons.VolumeUpIcon - -@Sampled -@Composable -fun LevelIndicatorSample() { - var value by remember { mutableFloatStateOf(0.5f) } - Box(modifier = Modifier.fillMaxSize()) { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize(), - ) { - IconButton( - modifier = Modifier.padding(horizontal = 16.dp), - enabled = value < 1f, - onClick = { value += 0.1f }, - ) { - VolumeUpIcon(StepperDefaults.IconSize) - } - IconButton( - modifier = Modifier.padding(horizontal = 16.dp), - enabled = value > 0f, - onClick = { value -= 0.1f }, - ) { - VolumeDownIcon(StepperDefaults.IconSize) - } - } - LevelIndicator(value = { value }, modifier = Modifier.align(Alignment.CenterStart)) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt deleted file mode 100644 index 8256973..0000000 --- a/wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt +++ /dev/null @@ -1,177 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.ListHeader -import androidx.wear.compose.material3.ListHeaderDefaults -import androidx.wear.compose.material3.ListSubHeader -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Sampled -@Preview -@Composable -fun ListHeaderSample() { - val transformationSpec = rememberTransformationSpec() - val scrollState = rememberTransformingLazyColumnState() - - AppScaffold { - ScreenScaffold(scrollState = scrollState) { contentPadding -> - TransformingLazyColumn( - state = scrollState, - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = contentPadding, - ) { - item { - ListHeader( - modifier = - Modifier.minimumVerticalContentPadding( - ListHeaderDefaults.minimumTopListContentPadding, - ListHeaderDefaults.minimumBottomListContentPadding, - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Settings") - } - } - item { - ListSubHeader( - modifier = - Modifier.minimumVerticalContentPadding( - ListHeaderDefaults.minimumTopListContentPadding, - ListHeaderDefaults.minimumBottomListContentPadding, - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - icon = { - Icon( - painter = painterResource(R.drawable.ic_connectivity), - contentDescription = "Connectivity", - ) - }, - label = { Text("Connectivity") }, - ) - } - item { - Button( - modifier = - Modifier.fillMaxWidth() - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - icon = { - Icon( - painter = painterResource(R.drawable.ic_bluetooth), - contentDescription = "Bluetooth", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - ) { - Text("Bluetooth") - } - } - item { - Button( - modifier = - Modifier.fillMaxWidth() - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - icon = { - Icon( - painter = painterResource(R.drawable.ic_wifi), - contentDescription = "Wifi", - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - }, - ) { - Text("Wifi") - } - } - item { - ListSubHeader( - modifier = - Modifier.minimumVerticalContentPadding( - ListHeaderDefaults.minimumTopListContentPadding, - ListHeaderDefaults.minimumBottomListContentPadding, - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Display") - } - } - item { - Button( - modifier = - Modifier.fillMaxWidth() - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text("Change Watchface") - } - } - item { - Button( - modifier = - Modifier.fillMaxWidth() - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - onClick = {}, - ) { - Text("Brightness") - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt b/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt deleted file mode 100644 index 340d53a..0000000 --- a/wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt +++ /dev/null @@ -1,651 +0,0 @@ -``` -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.annotation.Sampled -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.rememberOverscrollEffect -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.AmbientMode -import androidx.wear.compose.foundation.LocalAmbientModeManager -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.foundation.pager.HorizontalPager -import androidx.wear.compose.foundation.pager.VerticalPager -import androidx.wear.compose.foundation.pager.rememberPagerState -import androidx.wear.compose.foundation.rememberAmbientModeManager -import androidx.wear.compose.material3.AnimatedPage -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.EdgeButton -import androidx.wear.compose.material3.HorizontalPagerScaffold -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SwitchButton -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.VerticalPagerScaffold -import androidx.wear.compose.material3.onehandedgesture.LocalOneHandedGestureEnabled -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureDefaults -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureHorizontalPageIndicator -import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePageIndicatorState -import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePriority -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicator -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicatorState -import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureVerticalPageIndicator -import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture -import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun OneHandedGestureButtonSample() { - var label by remember { mutableStateOf("Gesturable Button") } - val onClick = { label = "Clicked/Gestured" } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.fillMaxWidth() - .oneHandedGesture( - gestureConfiguration = gestureConfig, - interactionSource = interactionSource, - onGestureLabel = "activate the button", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - onGesture = onClick, - ), - ) { - OneHandedGestureClickIndicator(gestureConfig, indicatorState) { - Text(label, modifier = Modifier.fillMaxWidth()) - } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureButtonInAmbientSample() { - var label by remember { mutableStateOf("Gesturable Button") } - val onClick = { label = "Clicked/Gestured" } - val interactionSource = remember { MutableInteractionSource() } - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - val coroutineScope = rememberCoroutineScope() - val activityAmbientModeManager = rememberAmbientModeManager() - var showGestureIndicator by remember { mutableStateOf(true) } - - CompositionLocalProvider(LocalAmbientModeManager provides activityAmbientModeManager) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - val isInAmbientMode = - LocalAmbientModeManager.current?.currentAmbientMode is AmbientMode.Ambient - Button( - onClick = onClick, - interactionSource = interactionSource, - modifier = - Modifier.fillMaxWidth() - .oneHandedGesture( - gestureConfiguration = gestureConfig, - interactionSource = interactionSource, - enabledInAmbient = true, - onGestureLabel = "activate the button", - onGestureAvailable = { - // Bypass repeating the gesture indicator on every recomposition. - if (showGestureIndicator) { - coroutineScope.launch { indicatorState.showIndicator() } - showGestureIndicator = false - } - }, - onGesture = onClick, - ), - colors = - if (isInAmbientMode) ButtonDefaults.outlinedButtonColors() - else ButtonDefaults.buttonColors(), - border = - if (isInAmbientMode) ButtonDefaults.outlinedButtonBorder(enabled = true) - else null, - ) { - OneHandedGestureClickIndicator(gestureConfig, indicatorState) { - Text(label, modifier = Modifier.fillMaxWidth()) - } - } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureDisableButtonSample() { - var counter by remember { mutableIntStateOf(0) } - var enabled by remember { mutableStateOf(true) } - val interactionSource = remember { MutableInteractionSource() } - val coroutineScope = rememberCoroutineScope() - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - SwitchButton(checked = enabled, onCheckedChange = { enabled = it }) { - Text("Gestures enabled") - } - Spacer(modifier = Modifier.height(6.dp)) - CompositionLocalProvider(LocalOneHandedGestureEnabled provides enabled) { - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGestureClickIndicatorState() } - Button( - onClick = {}, - interactionSource = interactionSource, - modifier = - Modifier.oneHandedGesture( - gestureConfiguration = gestureConfig, - interactionSource = interactionSource, - onGestureLabel = "increase the counter", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - onGesture = { counter++ }, - ), - ) { - OneHandedGestureClickIndicator(gestureConfig, indicatorState) { - Text("Gestured $counter times") - } - } - } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureTransformingLazyColumnSample() { - val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - val onClick = - remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } - val scrollState = rememberTransformingLazyColumnState() - val coroutineScope = rememberCoroutineScope() - - val buttonInteractionSource = remember { MutableInteractionSource() } - val buttonGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Clickable, - ) - val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } - - val scrollGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Scrollable, - ) - val scrollIndicatorState = - remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } - - ScreenScaffold( - scrollState = scrollState, - edgeButton = { - EdgeButton( - onClick = onClick, - interactionSource = buttonInteractionSource, - modifier = - if (scrollState.canScrollForward) { - Modifier - } else { - // Apply the one-handed gesture modifier only when the container cannot - // scroll further, ensuring the EdgeButton is fully visible and interactive - Modifier.oneHandedGesture( - gestureConfiguration = buttonGestureConfig, - interactionSource = buttonInteractionSource, - onGestureLabel = "close", - onGestureAvailable = { - coroutineScope.launch { buttonIndicatorState.showIndicator() } - }, - onGesture = onClick, - ) - } then - Modifier.scrollable( - state = scrollState, - orientation = Orientation.Vertical, - reverseDirection = true, - overscrollEffect = rememberOverscrollEffect(), - ), - ) { - OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { - Text("Close") - } - } - }, - scrollIndicator = { - OneHandedGestureScrollIndicator( - gestureConfiguration = scrollGestureConfig, - indicatorState = scrollIndicatorState, - scrollState = scrollState, - modifier = Modifier.align(Alignment.CenterEnd), - ) - }, - ) { contentPadding -> - TransformingLazyColumn( - state = scrollState, - contentPadding = contentPadding, - modifier = - Modifier.fillMaxSize() - .oneHandedGesture( - gestureConfiguration = scrollGestureConfig, - onGestureLabel = "scroll", - onGestureAvailable = { - coroutineScope.launch { scrollIndicatorState.showIndicator() } - }, - onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }, - ), - ) { - items(10) { Text("Item $it") } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureScalingLazyColumnSample() { - val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - val onClick = - remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } - val scrollState = rememberScalingLazyListState() - val coroutineScope = rememberCoroutineScope() - - val buttonInteractionSource = remember { MutableInteractionSource() } - val buttonGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Clickable, - ) - val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } - - val scrollGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Scrollable, - ) - val scrollIndicatorState = - remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } - - ScreenScaffold( - scrollState = scrollState, - edgeButton = { - EdgeButton( - onClick = onClick, - interactionSource = buttonInteractionSource, - modifier = - if (scrollState.canScrollForward) { - Modifier - } else { - // Apply the one-handed gesture modifier only when the container cannot - // scroll further, ensuring the EdgeButton is fully visible and interactive - Modifier.oneHandedGesture( - gestureConfiguration = buttonGestureConfig, - interactionSource = buttonInteractionSource, - onGestureLabel = "close", - onGestureAvailable = { - coroutineScope.launch { buttonIndicatorState.showIndicator() } - }, - onGesture = onClick, - ) - } then - Modifier.scrollable( - state = scrollState, - orientation = Orientation.Vertical, - reverseDirection = true, - overscrollEffect = rememberOverscrollEffect(), - ), - ) { - OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { - Text("Close") - } - } - }, - scrollIndicator = { - OneHandedGestureScrollIndicator( - gestureConfiguration = scrollGestureConfig, - indicatorState = scrollIndicatorState, - scrollState = scrollState, - modifier = Modifier.align(Alignment.CenterEnd), - ) - }, - ) { contentPadding -> - ScalingLazyColumn( - state = scrollState, - contentPadding = contentPadding, - modifier = - Modifier.fillMaxSize() - .oneHandedGesture( - gestureConfiguration = scrollGestureConfig, - onGestureLabel = "scroll", - onGestureAvailable = { - coroutineScope.launch { scrollIndicatorState.showIndicator() } - }, - onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }, - ), - autoCentering = null, - ) { - items(10) { Text("Item $it") } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureTransformingLazyColumnScrollToNextItemSample() { - val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - val onClick = - remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } - val scrollState = rememberTransformingLazyColumnState() - val coroutineScope = rememberCoroutineScope() - - val buttonInteractionSource = remember { MutableInteractionSource() } - val buttonGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Clickable, - ) - val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } - - val scrollGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Scrollable, - ) - val scrollIndicatorState = - remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } - - ScreenScaffold( - scrollState = scrollState, - edgeButton = { - EdgeButton( - onClick = onClick, - interactionSource = buttonInteractionSource, - modifier = - if (scrollState.canScrollForward) { - Modifier - } else { - // Apply the one-handed gesture modifier only when the container cannot - // scroll further, ensuring the EdgeButton is fully visible and interactive - Modifier.oneHandedGesture( - gestureConfiguration = buttonGestureConfig, - interactionSource = buttonInteractionSource, - onGestureLabel = "close", - onGestureAvailable = { - coroutineScope.launch { buttonIndicatorState.showIndicator() } - }, - onGesture = onClick, - ) - } then - Modifier.scrollable( - state = scrollState, - orientation = Orientation.Vertical, - reverseDirection = true, - overscrollEffect = rememberOverscrollEffect(), - ), - ) { - OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { - Text("Close") - } - } - }, - scrollIndicator = { - OneHandedGestureScrollIndicator( - gestureConfiguration = scrollGestureConfig, - indicatorState = scrollIndicatorState, - scrollState = scrollState, - modifier = Modifier.align(Alignment.CenterEnd), - ) - }, - ) { contentPadding -> - TransformingLazyColumn( - state = scrollState, - contentPadding = contentPadding, - modifier = - Modifier.fillMaxSize() - .oneHandedGesture( - gestureConfiguration = scrollGestureConfig, - onGestureLabel = "scroll", - onGestureAvailable = { - coroutineScope.launch { scrollIndicatorState.showIndicator() } - }, - onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) }, - ), - ) { - items(10) { Text("Item $it") } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureScalingLazyColumnScrollToNextItemSample() { - val backDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - val onClick = - remember<() -> Unit> { { backDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() } } - val scrollState = rememberScalingLazyListState() - val coroutineScope = rememberCoroutineScope() - - val buttonInteractionSource = remember { MutableInteractionSource() } - val buttonGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Clickable, - ) - val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } - - val scrollGestureConfig = - rememberOneHandedGestureConfiguration( - action = OneHandedGestureAction.Primary, - priority = OneHandedGesturePriority.Scrollable, - ) - val scrollIndicatorState = - remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } - - ScreenScaffold( - scrollState = scrollState, - edgeButton = { - EdgeButton( - onClick = onClick, - interactionSource = buttonInteractionSource, - modifier = - if (scrollState.canScrollForward) { - Modifier - } else { - // Apply the one-handed gesture modifier only when the container cannot - // scroll further, ensuring the EdgeButton is fully visible and interactive - Modifier.oneHandedGesture( - gestureConfiguration = buttonGestureConfig, - interactionSource = buttonInteractionSource, - onGestureLabel = "close", - onGestureAvailable = { - coroutineScope.launch { buttonIndicatorState.showIndicator() } - }, - onGesture = onClick, - ) - } then - Modifier.scrollable( - state = scrollState, - orientation = Orientation.Vertical, - reverseDirection = true, - overscrollEffect = rememberOverscrollEffect(), - ), - ) { - OneHandedGestureClickIndicator(buttonGestureConfig, buttonIndicatorState) { - Text("Close") - } - } - }, - scrollIndicator = { - OneHandedGestureScrollIndicator( - gestureConfiguration = scrollGestureConfig, - indicatorState = scrollIndicatorState, - scrollState = scrollState, - modifier = Modifier.align(Alignment.CenterEnd), - ) - }, - ) { contentPadding -> - ScalingLazyColumn( - state = scrollState, - contentPadding = contentPadding, - modifier = - Modifier.fillMaxSize() - .oneHandedGesture( - gestureConfiguration = scrollGestureConfig, - onGestureLabel = "scroll", - onGestureAvailable = { - coroutineScope.launch { scrollIndicatorState.showIndicator() } - }, - onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) }, - ), - autoCentering = null, - ) { - items(10) { Text("Item $it") } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureHorizontalPagerSample() { - val pagerState = rememberPagerState(pageCount = { 10 }) - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGesturePageIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - HorizontalPagerScaffold( - pagerState = pagerState, - pageIndicator = { - OneHandedGestureHorizontalPageIndicator( - gestureConfiguration = gestureConfig, - indicatorState = indicatorState, - pagerState = pagerState, - ) - }, - ) { - HorizontalPager( - state = pagerState, - modifier = - Modifier.oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "scroll to the next page", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - ) { - OneHandedGestureDefaults.scrollToNextPage(pagerState) - }, - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe left and right") - } - } - } - } - } -} - -@Sampled -@Composable -fun OneHandedGestureVerticalPagerSample() { - val pagerState = rememberPagerState(pageCount = { 10 }) - val gestureConfig = - rememberOneHandedGestureConfiguration(action = OneHandedGestureAction.Primary) - val indicatorState = remember { OneHandedGesturePageIndicatorState() } - val coroutineScope = rememberCoroutineScope() - - VerticalPagerScaffold( - pagerState = pagerState, - pageIndicator = { - OneHandedGestureVerticalPageIndicator( - gestureConfiguration = gestureConfig, - indicatorState = indicatorState, - pagerState = pagerState, - ) - }, - ) { - VerticalPager( - state = pagerState, - modifier = - Modifier.oneHandedGesture( - gestureConfiguration = gestureConfig, - onGestureLabel = "scroll to the next page", - onGestureAvailable = { - coroutineScope.launch { indicatorState.showIndicator() } - }, - ) { - OneHandedGestureDefaults.scrollToNextPage(pagerState) - }, - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe up and down") - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt b/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt deleted file mode 100644 index dc164fc..0000000 --- a/wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt +++ /dev/null @@ -1,57 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.OpenOnPhoneDialog -import androidx.wear.compose.material3.OpenOnPhoneDialogDefaults -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.openOnPhoneDialogCurvedText - -@Sampled -@Composable -fun OpenOnPhoneDialogSample() { - var showConfirmation by remember { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - FilledTonalButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showConfirmation = true }, - label = { Text("Open on phone") }, - ) - } - - val text = OpenOnPhoneDialogDefaults.text - val style = OpenOnPhoneDialogDefaults.curvedTextStyle - OpenOnPhoneDialog( - visible = showConfirmation, - onDismissRequest = { showConfirmation = false }, - curvedText = { openOnPhoneDialogCurvedText(text = text, style = style) }, - ) -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt deleted file mode 100644 index 6b5828e..0000000 --- a/wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt +++ /dev/null @@ -1,110 +0,0 @@ -``` -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.pager.HorizontalPager -import androidx.wear.compose.foundation.pager.VerticalPager -import androidx.wear.compose.foundation.pager.rememberPagerState -import androidx.wear.compose.material3.AnimatedPage -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.HorizontalPageIndicator -import androidx.wear.compose.material3.HorizontalPagerScaffold -import androidx.wear.compose.material3.PagerScaffoldDefaults -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.VerticalPageIndicator -import androidx.wear.compose.material3.VerticalPagerScaffold - -@Sampled -@Composable -fun HorizontalPageIndicatorWithPagerSample(navigateBack: () -> Unit) { - val pageCount = 9 - val pagerState = rememberPagerState { pageCount } - - Box { - HorizontalPagerScaffold( - pagerState = pagerState, - pageIndicator = { HorizontalPageIndicator(pagerState = pagerState) }, - ) { - HorizontalPager( - state = pagerState, - flingBehavior = - PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState), - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe left and right") - if (page == 0) { - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = navigateBack) { Text("Exit") } - } - } - } - } - } - } -} - -@Sampled -@Composable -fun VerticalPageIndicatorWithPagerSample() { - val pageCount = 9 - val pagerState = rememberPagerState { pageCount } - - Box { - VerticalPagerScaffold( - pagerState = pagerState, - pageIndicator = { VerticalPageIndicator(pagerState = pagerState) }, - ) { - VerticalPager( - state = pagerState, - flingBehavior = - PagerScaffoldDefaults.snapWithSpringFlingBehavior(state = pagerState), - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe up and down") - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt deleted file mode 100644 index 1d2aced..0000000 --- a/wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt +++ /dev/null @@ -1,204 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.pager.HorizontalPager -import androidx.wear.compose.foundation.pager.PagerDefaults -import androidx.wear.compose.foundation.pager.VerticalPager -import androidx.wear.compose.foundation.pager.rememberPagerState -import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults -import androidx.wear.compose.material3.AnimatedPage -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.HorizontalPagerScaffold -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.PagerScaffoldDefaults -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.VerticalPagerScaffold - -@Sampled -@Composable -fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) { - AppScaffold { - val pagerState = rememberPagerState(pageCount = { 10 }) - - HorizontalPagerScaffold(pagerState = pagerState) { - HorizontalPager( - state = pagerState, - flingBehavior = - PagerDefaults.snapFlingBehavior( - state = pagerState, - maxFlingPages = 1, - snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold, - snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), - ), - rotaryScrollableBehavior = null, - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe left and right") - if (page == 0) { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = navigateBack) { Text("Exit") } - } - } - } - } - } - } - } -} - -@Sampled -@Composable -fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) { - AppScaffold { - val pagerState = rememberPagerState(pageCount = { 3 }) - - HorizontalPagerScaffold(pagerState = pagerState) { - HorizontalPager( - state = pagerState, - flingBehavior = - PagerDefaults.snapFlingBehavior( - state = pagerState, - maxFlingPages = 0, - snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, - snapAnimationSpec = PagerDefaults.SnapAnimationSpec, - ), - rotaryScrollableBehavior = - RotaryScrollableDefaults.snapBehavior( - pagerState = pagerState, - snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity, - ), - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe left and right") - if (page == 0) { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = navigateBack) { Text("Exit") } - } - } - } - } - } - } - } -} - -@Sampled -@Composable -fun VerticalPagerScaffoldSample() { - AppScaffold { - val pagerState = rememberPagerState(pageCount = { 10 }) - - VerticalPagerScaffold(pagerState = pagerState) { - VerticalPager( - state = pagerState, - flingBehavior = - PagerDefaults.snapFlingBehavior( - state = pagerState, - maxFlingPages = 1, - snapPositionalThreshold = PagerScaffoldDefaults.HighSnapPositionalThreshold, - snapAnimationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), - ), - rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(pagerState), - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe up and down") - } - } - } - } - } - } -} - -@Sampled -@Composable -fun VerticalPagerScaffoldWithLowSensitivitySample() { - AppScaffold { - val pagerState = rememberPagerState(pageCount = { 3 }) - - VerticalPagerScaffold(pagerState = pagerState) { - VerticalPager( - state = pagerState, - flingBehavior = - PagerDefaults.snapFlingBehavior( - state = pagerState, - maxFlingPages = 0, - snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, - snapAnimationSpec = PagerDefaults.SnapAnimationSpec, - ), - rotaryScrollableBehavior = - RotaryScrollableDefaults.snapBehavior( - pagerState = pagerState, - snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity, - ), - ) { page -> - AnimatedPage(pageIndex = page, pagerState = pagerState) { - ScreenScaffold { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Page #$page") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Swipe up and down") - } - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt deleted file mode 100644 index 530b02f..0000000 --- a/wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt +++ /dev/null @@ -1,111 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.Picker -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.rememberPickerState -import kotlinx.coroutines.launch - -@Sampled -@Composable -fun SimplePicker() { - val items = listOf("One", "Two", "Three", "Four", "Five") - val state = rememberPickerState(items.size) - // We forward scroll gestures from the whole screen to the Picker which makes this sample - // accessible for 2-finger vertical scrolling. - Box( - modifier = - Modifier.fillMaxSize() - .scrollable( - state = state, - orientation = Orientation.Vertical, - reverseDirection = true, - ), - contentAlignment = Alignment.Center, - ) { - val selectedLabel by remember { - derivedStateOf { "Selected: ${items[state.selectedOptionIndex]}" } - } - Text( - modifier = Modifier.align(Alignment.TopCenter).padding(top = 10.dp), - text = selectedLabel, - ) - Picker( - modifier = Modifier.size(100.dp, 100.dp), - state = state, - contentDescription = { "${state.selectedOptionIndex + 1}" }, - ) { - Text(items[it]) - } - } -} - -@Sampled -@Composable -fun PickerScrollToOption() { - val coroutineScope = rememberCoroutineScope() - val state = rememberPickerState(initialNumberOfOptions = 10) - Picker( - state = state, - verticalSpacing = 4.dp, - contentDescription = { "${state.selectedOptionIndex + 1}" }, - ) { - Button( - onClick = { coroutineScope.launch { state.scrollToOption(it) } }, - label = { Text("$it") }, - ) - } -} - -@Sampled -@Composable -fun PickerAnimateScrollToOption() { - val coroutineScope = rememberCoroutineScope() - val state = rememberPickerState(initialNumberOfOptions = 10) - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Picker( - state = state, - verticalSpacing = 4.dp, - contentDescription = { "${state.selectedOptionIndex + 1}" }, - ) { - Button( - onClick = { coroutineScope.launch { state.animateScrollToOption(it) } }, - label = { Text("$it") }, - ) - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt deleted file mode 100644 index 943bca1..0000000 --- a/wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt +++ /dev/null @@ -1,182 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.placeholder -import androidx.wear.compose.material3.placeholderShimmer -import androidx.wear.compose.material3.rememberPlaceholderState -import kotlinx.coroutines.delay - -/** - * This sample applies placeholders directly over the content that is waiting to be loaded. This - * approach is suitable for situations where the developer has an approximate knowledge of how big - * the content is going to be and it doesn't have cached data that can be shown. - */ -@Sampled -@Composable -fun ButtonWithIconAndLabelAndPlaceholders() { - var labelText by remember { mutableStateOf("") } - var imageVector: ImageVector? by remember { mutableStateOf(null) } - val buttonPlaceholderState = - rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null) - - FilledTonalButton( - onClick = { /* Do something */ }, - enabled = true, - modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState), - label = { - Text( - text = labelText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth().placeholder(buttonPlaceholderState), - ) - }, - icon = { - Box( - modifier = - Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState) - ) { - if (imageVector != null) { - Icon( - imageVector = imageVector!!, - contentDescription = "Heart", - modifier = - Modifier.wrapContentSize(align = Alignment.Center) - .size(ButtonDefaults.IconSize) - .fillMaxSize(), - ) - } - } - }, - ) - // Simulate content loading completing in stages - LaunchedEffect(Unit) { - delay(2000) - imageVector = Icons.Filled.Favorite - delay(1000) - labelText = "A label" - } -} - -/** - * This sample doesn't use placeholders for the label as there is some cached data that can be shown - * while loading. - */ -@Sampled -@Composable -fun ButtonWithIconAndLabelCachedData() { - var labelText by remember { mutableStateOf("Cached Data") } - var imageVector: ImageVector? by remember { mutableStateOf(null) } - val buttonPlaceholderState = - rememberPlaceholderState(isVisible = labelText.isEmpty() || imageVector == null) - - // Put placeholderShimmer in the container and placeholder in the elements of the content that - // have no cached data. - FilledTonalButton( - onClick = { /* Do something */ }, - enabled = true, - modifier = Modifier.fillMaxWidth().placeholderShimmer(buttonPlaceholderState), - label = { - Text( - text = labelText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(), - ) - }, - icon = { - Box( - modifier = - Modifier.size(ButtonDefaults.IconSize).placeholder(buttonPlaceholderState) - ) { - if (imageVector != null) { - Icon( - imageVector = imageVector!!, - contentDescription = "Heart", - modifier = - Modifier.wrapContentSize(align = Alignment.Center) - .size(ButtonDefaults.IconSize) - .fillMaxSize(), - ) - } - } - }, - ) - // Simulate content loading completing in stages - LaunchedEffect(Unit) { - delay(2000) - imageVector = Icons.Filled.Favorite - delay(1000) - labelText = "A label" - } -} - -/** - * This sample applies a placeholder and placeholderShimmer directly over a single composable. - * - * Note that the modifier ordering is important, the placeholderShimmer must be before the - * placeholder in the modifier chain - otherwise the shimmer will be drawn underneath the - * placeholder and will not be visible. - */ -@Sampled -@Composable -fun TextPlaceholder() { - var labelText by remember { mutableStateOf("") } - val placeholderState = rememberPlaceholderState(isVisible = labelText.isEmpty()) - - Text( - text = labelText, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = - Modifier.width(90.dp).placeholderShimmer(placeholderState).placeholder(placeholderState), - ) - - // Simulate content loading - LaunchedEffect(Unit) { - delay(3000) - labelText = "A label" - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt deleted file mode 100644 index 97332f1..0000000 --- a/wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt +++ /dev/null @@ -1,263 +0,0 @@ -``` -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.ArcProgressIndicator -import androidx.wear.compose.material3.ArcProgressIndicatorDefaults -import androidx.wear.compose.material3.CircularProgressIndicator -import androidx.wear.compose.material3.CircularProgressIndicatorDefaults -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.IconButton -import androidx.wear.compose.material3.IconButtonDefaults -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.ProgressIndicatorDefaults -import androidx.wear.compose.material3.SegmentedCircularProgressIndicator -import androidx.wear.compose.material3.drawCircularProgressIndicator - -@Sampled -@Composable -fun FullScreenProgressIndicatorSample() { - Box( - modifier = - Modifier.background(MaterialTheme.colorScheme.background) - .padding(CircularProgressIndicatorDefaults.FullScreenPadding) - .fillMaxSize() - ) { - CircularProgressIndicator(progress = { 0.25f }, startAngle = 120f, endAngle = 60f) - } -} - -@Sampled -@Composable -fun MediaButtonProgressIndicatorSample() { - var isPlaying by remember { mutableStateOf(false) } - val buttonPadding = 4.dp - val progressStrokeWidth = 4.dp - val progress = 0.75f - - Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { - // The CircularProgressIndicator should be around the IconButton, with an extra gap between - // then of 'buttonPadding'. We multiply by 2 because the size includes progressStrokeWidth - // at top and bottom and the buttonPadding at top and bottom. - CircularProgressIndicator( - modifier = - Modifier.align(Alignment.Center) - .size( - IconButtonDefaults.DefaultButtonSize + - progressStrokeWidth * 2 + - buttonPadding * 2 - ), - progress = { progress }, - strokeWidth = progressStrokeWidth, - ) - - IconButton( - modifier = - Modifier.align(Alignment.Center) - .semantics { - // Set custom progress semantics for accessibility. - contentDescription = - String.format( - "Play/pause button, track progress: %.0f%%", - progress * 100, - ) - } - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerLow), - onClick = { isPlaying = !isPlaying }, - ) { - Icon( - imageVector = if (isPlaying) Icons.Filled.Close else Icons.Filled.PlayArrow, - contentDescription = null, - ) - } - } -} - -@Sampled -@Composable -fun OverflowProgressIndicatorSample() { - Box( - modifier = - Modifier.background(MaterialTheme.colorScheme.background) - .padding(CircularProgressIndicatorDefaults.FullScreenPadding) - .fillMaxSize() - ) { - CircularProgressIndicator( - // Overflow value of 120% - progress = { 1.2f }, - allowProgressOverflow = true, - startAngle = 120f, - endAngle = 60f, - ) - } -} - -@Sampled -@Composable -fun SmallValuesProgressIndicatorSample() { - Box { - CircularProgressIndicator( - // Small progress values like 2% will be rounded up to at least the stroke width. - progress = { 0.02f }, - modifier = - Modifier.fillMaxSize().padding(CircularProgressIndicatorDefaults.FullScreenPadding), - startAngle = 120f, - endAngle = 60f, - strokeWidth = 10.dp, - colors = - ProgressIndicatorDefaults.colors( - indicatorColor = Color.Green, - trackColor = Color.White, - ), - ) - } -} - -@Sampled -@Composable -fun CircularProgressIndicatorCustomAnimationSample() { - val animatedProgress = remember { Animatable(0f) } - val colors = - ProgressIndicatorDefaults.colors(indicatorColor = Color.Green, trackColor = Color.White) - - LaunchedEffect(Unit) { - animatedProgress.animateTo(1f, tween(durationMillis = 1024, easing = LinearEasing)) - animatedProgress.animateTo(0f, tween(durationMillis = 1024, easing = LinearEasing)) - } - - Box( - modifier = - Modifier.background(MaterialTheme.colorScheme.background) - .padding(CircularProgressIndicatorDefaults.FullScreenPadding) - .fillMaxSize() - ) { - // Draw the circular progress indicator with custom animation - Spacer( - Modifier.fillMaxSize().focusable().drawBehind { - drawCircularProgressIndicator( - progress = animatedProgress.value, - strokeWidth = 10.dp, - colors = colors, - startAngle = 120f, - endAngle = 60f, - ) - } - ) - } -} - -@Sampled -@Composable -fun IndeterminateProgressIndicatorSample() { - Box(modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } -} - -@Sampled -@Composable -fun IndeterminateProgressArcSample() { - Box(modifier = Modifier.fillMaxSize()) { - ArcProgressIndicator( - modifier = - Modifier.align(Alignment.Center) - .size(ArcProgressIndicatorDefaults.recommendedIndeterminateDiameter) - ) - } -} - -@Sampled -@Composable -fun SegmentedProgressIndicatorSample() { - Box( - modifier = - Modifier.background(MaterialTheme.colorScheme.background) - .padding(CircularProgressIndicatorDefaults.FullScreenPadding) - .fillMaxSize() - ) { - SegmentedCircularProgressIndicator(segmentCount = 5, progress = { 0.5f }) - } -} - -@Sampled -@Composable -fun SegmentedProgressIndicatorBinarySample() { - Box( - modifier = - Modifier.background(MaterialTheme.colorScheme.background) - .padding(CircularProgressIndicatorDefaults.FullScreenPadding) - .fillMaxSize() - ) { - SegmentedCircularProgressIndicator(segmentCount = 5, segmentValue = { it % 2 != 0 }) - } -} - -@Sampled -@Composable -fun SmallSegmentedProgressIndicatorSample() { - Box(modifier = Modifier.fillMaxSize()) { - SegmentedCircularProgressIndicator( - segmentCount = 6, - progress = { 0.75f }, - modifier = Modifier.align(Alignment.Center).size(80.dp), - ) - } -} - -@Sampled -@Composable -fun SmallSegmentedProgressIndicatorBinarySample() { - Box(modifier = Modifier.fillMaxSize()) { - SegmentedCircularProgressIndicator( - segmentCount = 8, - segmentValue = { it % 2 != 0 }, - modifier = Modifier.align(Alignment.Center).size(80.dp), - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt deleted file mode 100644 index 3c1b941..0000000 --- a/wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt +++ /dev/null @@ -1,109 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.RadioButton -import androidx.wear.compose.material3.SplitRadioButton -import androidx.wear.compose.material3.Text - -@Sampled -@Preview -@Composable -fun RadioButtonSample() { - Column(modifier = Modifier.selectableGroup().fillMaxSize()) { - var selectedButton by remember { mutableStateOf(0) } - // RadioButton uses the Radio selection control by default. - RadioButton( - label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - secondaryLabel = { - Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) - }, - selected = selectedButton == 0, - onSelect = { selectedButton = 0 }, - icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, - enabled = true, - ) - Spacer(modifier = Modifier.height(4.dp)) - RadioButton( - label = { Text("Radio button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - secondaryLabel = { - Text("With secondary label", maxLines = 3, overflow = TextOverflow.Ellipsis) - }, - selected = selectedButton == 1, - onSelect = { selectedButton = 1 }, - icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, - enabled = true, - ) - } -} - -@Sampled -@Preview -@Composable -fun SplitRadioButtonSample() { - Column(modifier = Modifier.selectableGroup().padding(horizontal = 10.dp)) { - var selectedButton by remember { mutableStateOf(0) } - // SplitRadioButton uses the Radio selection control by default. - SplitRadioButton( - label = { Text("First Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - selected = selectedButton == 0, - onSelectionClick = { selectedButton = 0 }, - selectionContentDescription = "First", - onContainerClick = { - /* Do something */ - }, - containerClickLabel = "click", - modifier = Modifier.fillMaxWidth(), - enabled = true, - ) - Spacer(modifier = Modifier.height(4.dp)) - SplitRadioButton( - label = { Text("Second Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - selected = selectedButton == 1, - onSelectionClick = { selectedButton = 1 }, - selectionContentDescription = "Second", - onContainerClick = { - /* Do something */ - }, - containerClickLabel = "click", - modifier = Modifier.fillMaxWidth(), - enabled = true, - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt deleted file mode 100644 index 9620d69..0000000 --- a/wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt +++ /dev/null @@ -1,146 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberOverscrollEffect -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.EdgeButton -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Preview -@Sampled -@Composable -fun ScaffoldSample() { - // Declare just one [AppScaffold] per app such as in the activity. - // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible - // during in-app transitions such as swipe-to-dismiss. - AppScaffold { - val transformationSpec = rememberTransformationSpec() - - // Define the navigation hierarchy within the AppScaffold, - // such as using SwipeDismissableNavHost. - // For this sample, we will define a single screen inline. - val listState = rememberTransformingLazyColumnState() - - // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator, - // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton. - ScreenScaffold(scrollState = listState) { contentPadding -> - TransformingLazyColumn( - state = listState, - contentPadding = contentPadding, - modifier = Modifier.fillMaxSize(), - ) { - items(10) { - Button( - onClick = {}, - label = { Text("Item ${it + 1}") }, - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .fillMaxWidth(), - ) - } - } - } - } -} - -@Preview -@Sampled -@Composable -fun ScaffoldWithTLCEdgeButtonSample() { - // Declare just one [AppScaffold] per app such as in the activity. - // [AppScaffold] allows static screen elements (i.e. [TimeText]) to remain visible - // during in-app transitions such as swipe-to-dismiss. - AppScaffold(modifier = Modifier.background(Color.Black)) { - val transformationSpec = rememberTransformationSpec() - - // Define the navigation hierarchy within the AppScaffold, - // such as using SwipeDismissableNavHost. - // For this sample, we will define a single screen inline. - val listState = rememberTransformingLazyColumnState() - // By default, ScreenScaffold will handle transitions showing/hiding ScrollIndicator, - // showing/hiding/scrolling away TimeText and optionally hosting the EdgeButton. - ScreenScaffold( - scrollState = listState, - // Define custom spacing between [EdgeButton] and [TransformingLazyColumn]. - edgeButtonSpacing = 15.dp, - edgeButton = { - EdgeButton( - onClick = {}, - modifier = - // In case user starts scrolling from the EdgeButton. - Modifier.scrollable( - listState, - orientation = Orientation.Vertical, - reverseDirection = true, - // An overscroll effect should be applied to the EdgeButton for proper - // scrolling behavior. - overscrollEffect = rememberOverscrollEffect(), - ), - ) { - Text("Clear All") - } - }, - ) { contentPadding -> - TransformingLazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - // Bottom spacing is derived from [ScreenScaffold.edgeButtonSpacing]. - contentPadding = contentPadding, - ) { - items(4) { - Button( - onClick = {}, - label = { Text("Item ${it + 1}") }, - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .fillMaxWidth(), - ) - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt deleted file mode 100644 index 7205759..0000000 --- a/wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt +++ /dev/null @@ -1,97 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.ScrollInfoProvider -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.ListHeader -import androidx.wear.compose.material3.ListHeaderDefaults -import androidx.wear.compose.material3.ScreenStage -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TimeText -import androidx.wear.compose.material3.curvedText -import androidx.wear.compose.material3.scrollAway -import androidx.wear.compose.material3.timeTextSeparator - -@Sampled -@Composable -fun ScrollAwaySample() { - val state = rememberTransformingLazyColumnState() - - Box(modifier = Modifier.fillMaxSize()) { - TransformingLazyColumn(state = state, modifier = Modifier.fillMaxSize()) { - item { - ListHeader( - modifier = - Modifier.minimumVerticalContentPadding( - ListHeaderDefaults.minimumTopListContentPadding, - ListHeaderDefaults.minimumBottomListContentPadding, - ) - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = "TLC", - textAlign = TextAlign.Center, - ) - } - } - items(50) { - FilledTonalButton( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = 36.dp) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ), - onClick = {}, - label = { Text("Item ${it + 1}") }, - ) - } - } - TimeText( - // In practice, it is recommended to use the [AppScaffold] and [ScreenScaffold], - // so that the Material3 scroll away behavior is provided by default, rather than using - // [Modifier.scrollAway] directly. - modifier = - Modifier.scrollAway( - scrollInfoProvider = ScrollInfoProvider(state), - screenStage = { - if (state.isScrollInProgress) ScreenStage.Scrolling else ScreenStage.Idle - }, - ), - content = { time -> - curvedText("List") - timeTextSeparator() - curvedText(time) - }, - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt b/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt deleted file mode 100644 index 95f8ab8..0000000 --- a/wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt +++ /dev/null @@ -1,66 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.ScreenScaffoldDefaults -import androidx.wear.compose.material3.ScrollIndicator -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TimeText - -@Sampled -@Preview -@Composable -fun ScrollIndicatorWithTLCSample() { - val scrollState = rememberTransformingLazyColumnState() - Box(modifier = Modifier.fillMaxSize()) { - TransformingLazyColumn( - modifier = Modifier.background(Color.Black), - state = scrollState, - contentPadding = ScreenScaffoldDefaults.contentPadding, - ) { - items(15) { - Button( - onClick = {}, - label = { Text("Button $it") }, - modifier = - Modifier.minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .fillMaxWidth(), - ) - } - } - ScrollIndicator(modifier = Modifier.align(Alignment.CenterEnd), state = scrollState) - TimeText() - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt deleted file mode 100644 index 25bb251..0000000 --- a/wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt +++ /dev/null @@ -1,86 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.wear.compose.material3.Slider -import androidx.wear.compose.material3.SliderDefaults - -@Sampled -@Composable -fun SliderSample() { - var value by remember { mutableStateOf(4.5f) } - Slider( - value = value, - onValueChange = { value = it }, - valueRange = 3f..6f, - steps = 5, - segmented = false, - ) -} - -@Sampled -@Composable -fun ChangedSliderSample() { - val initialValue = 4.5f - var value by remember { mutableStateOf(5.0f) } - Slider( - value = value, - onValueChange = { value = it }, - valueRange = 3f..6f, - steps = 5, - segmented = false, - colors = - if (value == initialValue) { - SliderDefaults.sliderColors() - } else { - SliderDefaults.variantSliderColors() - }, - ) -} - -@Sampled -@Composable -fun SliderSegmentedSample() { - var value by remember { mutableStateOf(2f) } - Slider( - value = value, - onValueChange = { value = it }, - valueRange = 1f..4f, - steps = 2, - segmented = true, - ) -} - -@Sampled -@Composable -fun SliderWithIntegerSample() { - var value by remember { mutableStateOf(4) } - Slider( - value = value, - onValueChange = { value = it }, - valueProgression = 0..10, - segmented = false, - ) -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt b/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt deleted file mode 100644 index ea7c164..0000000 --- a/wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt +++ /dev/null @@ -1,148 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.Stepper -import androidx.wear.compose.material3.StepperDefaults -import androidx.wear.compose.material3.StepperLevelIndicator -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.rangeSemantics -import androidx.wear.compose.material3.samples.icons.HeadphoneIcon -import androidx.wear.compose.material3.samples.icons.VolumeDownIcon -import androidx.wear.compose.material3.samples.icons.VolumeUpIcon - -@Sampled -@Composable -fun StepperSample() { - var value by remember { mutableFloatStateOf(2f) } - val valueRange = remember { 0f..4f } - Box(modifier = Modifier.fillMaxSize()) { - Stepper( - value = value, - onValueChange = { value = it }, - valueRange = valueRange, - steps = 7, - decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, - increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, - ) { - Text(String.format("Value: %.1f".format(value))) - } - StepperLevelIndicator( - value = { value }, - valueRange = valueRange, - modifier = Modifier.align(Alignment.CenterStart), - ) - } -} - -@Sampled -@Composable -fun StepperWithIntegerSample() { - var value by remember { mutableIntStateOf(3) } - val valueProgression = remember { 0..10 } - Box(modifier = Modifier.fillMaxSize()) { - Stepper( - value = value, - onValueChange = { value = it }, - valueProgression = valueProgression, - decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, - increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, - ) { - Text(String.format("Value: %d".format(value))) - } - StepperLevelIndicator( - value = { value }, - valueProgression = valueProgression, - modifier = Modifier.align(Alignment.CenterStart), - ) - } -} - -@Sampled -@Composable -fun StepperWithRangeSemanticsSample() { - var value by remember { mutableFloatStateOf(2f) } - val valueRange = remember { 0f..4f } - val onValueChange = { i: Float -> value = i } - val steps = 7 - Box(modifier = Modifier.fillMaxSize()) { - Stepper( - value = value, - onValueChange = onValueChange, - valueRange = valueRange, - modifier = Modifier.rangeSemantics(value, true, onValueChange, valueRange, steps), - steps = steps, - decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, - increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, - ) { - Text("Value: $value") - } - StepperLevelIndicator( - value = { value }, - valueRange = valueRange, - modifier = Modifier.align(Alignment.CenterStart), - ) - } -} - -@Sampled -@Composable -fun StepperWithButtonSample() { - var value by remember { mutableFloatStateOf(2f) } - val valueRange = remember { 0f..4f } - Box(modifier = Modifier.fillMaxSize()) { - Stepper( - value = value, - onValueChange = { value = it }, - valueRange = valueRange, - increaseIcon = { VolumeUpIcon(StepperDefaults.IconSize) }, - decreaseIcon = { VolumeDownIcon(StepperDefaults.IconSize) }, - steps = 7, - ) { - Text(String.format("Value: %.1f".format(value))) - Button( - onClick = {}, - modifier = Modifier.width(150.dp), - label = { Text(text = "This watch", modifier = Modifier.fillMaxWidth()) }, - secondaryLabel = { Text(text = "Headphones", modifier = Modifier.fillMaxWidth()) }, - icon = { HeadphoneIcon(24.dp) }, - ) - } - StepperLevelIndicator( - value = { value }, - valueRange = valueRange, - modifier = Modifier.align(Alignment.CenterStart), - ) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt deleted file mode 100644 index 6b3b7fa..0000000 --- a/wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt +++ /dev/null @@ -1,165 +0,0 @@ -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.paint -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.CardDefaults -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TitleCard -import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec -import androidx.wear.compose.material3.lazy.TransformationVariableSpec -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Sampled -@Preview -@Composable -fun SurfaceTransformationOnCustomComponent() { - @Composable - fun MyCardComponent( - title: String, - body: String, - transformation: SurfaceTransformation, - modifier: Modifier = Modifier, - ) { - Column( - modifier = - modifier - .fillMaxWidth() - .paint( - transformation.createContainerPainter( - ColorPainter(color = Color.Gray), - shape = RoundedCornerShape(16.dp), - ) - ) - .graphicsLayer { with(transformation) { applyContainerTransformation() } } - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Text(title) - Text(body) - } - } - - val transformationSpec = rememberTransformationSpec() - - TransformingLazyColumn { - items(count = 100) { - MyCardComponent( - "Message #$it", - "This is a body", - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ), - ) - } - } -} - -@Sampled -@Preview -@Composable -fun SurfaceTransformationButtonSample() { - val transformationSpec = - rememberTransformationSpec( - ResponsiveTransformationSpec.smallScreen( - contentAlpha = - TransformationVariableSpec( - 0f, - transformationZoneEnterFraction = 0.4f, - transformationZoneExitFraction = 0.8f, - ), - containerAlpha = TransformationVariableSpec(0.3f), - ) - ) - - TransformingLazyColumn { - items(count = 100) { - Button( - onClick = {}, - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ), - ) { - Text("Button #$it") - } - } - } -} - -@Sampled -@Preview -@Composable -fun SurfaceTransformationCardSample() { - val transformationSpec = rememberTransformationSpec() - var expandedIndex by remember { mutableIntStateOf(-1) } - val state = rememberTransformingLazyColumnState() - - AppScaffold { - ScreenScaffold(state) { contentPadding -> - TransformingLazyColumn(state = state, contentPadding = contentPadding) { - items(count = 100) { - TitleCard( - onClick = { expandedIndex = if (expandedIndex == it) -1 else it }, - title = { Text("Card #$it") }, - subtitle = { Text("Subtitle #$it") }, - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ), - ) { - if (it == expandedIndex) { - Text("Expanded content #$it") - } - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt deleted file mode 100644 index 75c908d..0000000 --- a/wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt +++ /dev/null @@ -1,171 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.SwipeToDismissValue -import androidx.wear.compose.foundation.edgeSwipeToDismiss -import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState -import androidx.wear.compose.material3.CheckboxButton -import androidx.wear.compose.material3.FilledTonalButton -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.SwipeToDismissBox -import androidx.wear.compose.material3.Text - -@Sampled -@Composable -fun SimpleSwipeToDismissBox(navigateBack: () -> Unit) { - SwipeToDismissBox(onDismissed = navigateBack) { isBackground -> - if (isBackground) { - Box( - modifier = - Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer) - ) - } else { - Column( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary) - } - } - } -} - -@Sampled -@Composable -fun StatefulSwipeToDismissBox() { - // State for managing a 2-level navigation hierarchy between - // MainScreen and ItemScreen composables. - // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation. - var showMainScreen by remember { mutableStateOf(true) } - val saveableStateHolder = rememberSaveableStateHolder() - - // Swipe gesture dismisses ItemScreen to return to MainScreen. - val state = rememberSwipeToDismissBoxState() - LaunchedEffect(state.currentValue) { - if (state.currentValue == SwipeToDismissValue.Dismissed) { - state.snapTo(SwipeToDismissValue.Default) - showMainScreen = !showMainScreen - } - } - - // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind - // the ItemScreen, otherwise there's no background to show. - SwipeToDismissBox( - state = state, - userSwipeEnabled = !showMainScreen, - backgroundKey = if (!showMainScreen) "MainKey" else "Background", - contentKey = if (showMainScreen) "MainKey" else "ItemKey", - ) { isBackground -> - if (isBackground || showMainScreen) { - // Best practice would be to use State Hoisting and leave this composable stateless. - // Here, we want to support MainScreen being shown from different destinations - // (either in the foreground or in the background during swiping) - that can be achieved - // using SaveableStateHolder and rememberSaveable as shown below. - saveableStateHolder.SaveableStateProvider( - key = "MainKey", - content = { - // Composable that maintains its own state - // and can be shown in foreground or background. - val checked = rememberSaveable { mutableStateOf(true) } - Column( - modifier = - Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), - ) { - FilledTonalButton( - onClick = { showMainScreen = false }, - modifier = Modifier.fillMaxWidth(), - ) { - Text("Item details") - } - CheckboxButton( - label = { Text("Checkbox", maxLines = 1) }, - checked = checked.value, - onCheckedChange = { checked.value = it }, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - ) - } else { - Column( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text("Show details here...", color = MaterialTheme.colorScheme.onPrimary) - Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary) - } - } - } -} - -@Sampled -@Composable -fun EdgeSwipeForSwipeToDismiss(navigateBack: () -> Unit) { - val state = rememberSwipeToDismissBoxState() - - // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the - // modifier applies exists within a SwipeToDismissBox which shares the same state. - SwipeToDismissBox(state = state, onDismissed = navigateBack) { isBackground -> - val horizontalScrollState = rememberScrollState(0) - if (isBackground) { - Box( - modifier = - Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer) - ) - } else { - Box(modifier = Modifier.fillMaxSize()) { - Text( - modifier = - Modifier.align(Alignment.Center) - .edgeSwipeToDismiss(state) - .horizontalScroll(horizontalScrollState), - text = - "This text can be scrolled horizontally - to dismiss, swipe " + - "right from the left edge of the screen (called Edge Swiping)", - ) - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt deleted file mode 100644 index 9e87918..0000000 --- a/wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt +++ /dev/null @@ -1,68 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.material3.Icon -import androidx.wear.compose.material3.SplitSwitchButton -import androidx.wear.compose.material3.SwitchButton -import androidx.wear.compose.material3.Text - -@Sampled -@Preview -@Composable -fun SwitchButtonSample() { - var checked by remember { mutableStateOf(true) } - SwitchButton( - label = { Text("Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - secondaryLabel = { - Text("With secondary label", maxLines = 2, overflow = TextOverflow.Ellipsis) - }, - checked = checked, - onCheckedChange = { checked = it }, - icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite icon") }, - enabled = true, - ) -} - -@Sampled -@Preview -@Composable -fun SplitSwitchButtonSample() { - var checked by remember { mutableStateOf(true) } - SplitSwitchButton( - label = { Text("Split Switch Button", maxLines = 3, overflow = TextOverflow.Ellipsis) }, - checked = checked, - onCheckedChange = { checked = it }, - toggleContentDescription = "Split Switch Button Sample", - onContainerClick = { - /* Do something */ - }, - enabled = true, - ) -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt deleted file mode 100644 index cf507f2..0000000 --- a/wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt +++ /dev/null @@ -1,115 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TextButton -import androidx.wear.compose.material3.TextButtonDefaults - -@Composable -@Sampled -fun TextButtonSample() { - TextButton(onClick = { /* Do something */ }) { Text(text = "ABC") } -} - -@Composable -@Sampled -fun FilledTextButtonSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.filledTextButtonColors(), - ) { - Text(text = "ABC") - } -} - -@Composable -@Sampled -fun FilledVariantTextButtonSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.filledVariantTextButtonColors(), - ) { - Text(text = "ABC") - } -} - -@Composable -@Sampled -fun LargeFilledTonalTextButtonSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.filledTonalTextButtonColors(), - modifier = Modifier.size(TextButtonDefaults.LargeButtonSize), - ) { - Text(text = "ABC", style = TextButtonDefaults.largeButtonTextStyle) - } -} - -@Composable -@Sampled -fun FilledTonalTextButtonSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.filledTonalTextButtonColors(), - ) { - Text(text = "ABC") - } -} - -@Composable -@Sampled -fun OutlinedTextButtonSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.outlinedTextButtonColors(), - border = ButtonDefaults.outlinedButtonBorder(enabled = true), - ) { - Text(text = "ABC") - } -} - -@Sampled -@Composable -fun TextButtonWithOnLongClickSample(onLongClick: () -> Unit) { - TextButton( - onClick = { /* Do something for onClick*/ }, - onLongClick = onLongClick, - onLongClickLabel = "Long click", - ) { - Text(text = "ABC") - } -} - -@Composable -@Sampled -fun TextButtonWithCornerAnimationSample() { - TextButton( - onClick = { /* Do something */ }, - colors = TextButtonDefaults.filledTextButtonColors(), - shapes = TextButtonDefaults.animatedShapes(), - ) { - Text(text = "ABC") - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt deleted file mode 100644 index 11f43da..0000000 --- a/wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt +++ /dev/null @@ -1,71 +0,0 @@ -``` -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TextButtonDefaults -import androidx.wear.compose.material3.TextToggleButton -import androidx.wear.compose.material3.TextToggleButtonDefaults -import androidx.wear.compose.material3.touchTargetAwareSize - -@Sampled -@Composable -fun TextToggleButtonSample() { - var checked by remember { mutableStateOf(true) } - TextToggleButton( - checked = checked, - onCheckedChange = { checked = !checked }, - shapes = TextToggleButtonDefaults.animatedShapes(), - ) { - Text(text = if (checked) "On" else "Off") - } -} - -@Sampled -@Composable -fun TextToggleButtonVariantSample() { - var checked by remember { mutableStateOf(true) } - TextToggleButton( - checked = checked, - onCheckedChange = { checked = !checked }, - shapes = TextToggleButtonDefaults.variantAnimatedShapes(), - ) { - Text(text = if (checked) "On" else "Off") - } -} - -@Sampled -@Composable -fun LargeTextToggleButtonSample() { - var checked by remember { mutableStateOf(true) } - TextToggleButton( - checked = checked, - onCheckedChange = { checked = !checked }, - modifier = Modifier.touchTargetAwareSize(TextButtonDefaults.LargeButtonSize), - ) { - Text(text = if (checked) "On" else "Off", style = TextToggleButtonDefaults.largeTextStyle) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt deleted file mode 100644 index fc15a6c..0000000 --- a/wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt +++ /dev/null @@ -1,65 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.style.TextOverflow -import androidx.wear.compose.foundation.CurvedModifier -import androidx.wear.compose.foundation.weight -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.TimeText -import androidx.wear.compose.material3.TimeTextDefaults -import androidx.wear.compose.material3.curvedText -import androidx.wear.compose.material3.timeTextCurvedText -import androidx.wear.compose.material3.timeTextSeparator - -@Sampled -@Composable -fun TimeTextClockOnly() { - // TimeText displays the current time by default. - TimeText() -} - -@Sampled -@Composable -fun TimeTextWithStatus() { - val style = TimeTextDefaults.timeTextStyle() - val primaryStyle = TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primary) - TimeText { time -> - timeTextCurvedText("ETA 12:48", style = primaryStyle) - timeTextSeparator(style) - timeTextCurvedText(time) - } -} - -@Sampled -@Composable -fun TimeTextWithStatusEllipsized() { - val style = TimeTextDefaults.timeTextStyle() - TimeText { time -> - curvedText( - "Long status that should be ellipsized.", - CurvedModifier.weight(1f), - overflow = TextOverflow.Ellipsis, - ) - timeTextSeparator(style) - timeTextCurvedText(time) - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt deleted file mode 100644 index 84704cb..0000000 --- a/wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt +++ /dev/null @@ -1,246 +0,0 @@ -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy.Companion.ModulateAlpha -import androidx.compose.ui.graphics.GraphicsLayerScope -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.TransformingLazyColumnItemScrollProgress -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.ButtonGroup -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.lazy.ResponsiveTransformationSpec -import androidx.wear.compose.material3.lazy.TransformationSpec -import androidx.wear.compose.material3.lazy.TransformationVariableSpec -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Composable -@Sampled -@Preview -fun CustomTransformationSpecSample() { - val transformationSpec = rememberTransformationSpec() - val morphingTransformationSpec = - object : TransformationSpec by transformationSpec { - override fun GraphicsLayerScope.applyContainerTransformation( - scrollProgress: TransformingLazyColumnItemScrollProgress - ) { - with(transformationSpec) { applyContainerTransformation(scrollProgress) } - rotationX = (scrollProgress.topOffsetFraction - 0.5f).coerceIn(0f..1f) * 270f - } - } - - TransformingLazyColumn( - contentPadding = PaddingValues(20.dp), - modifier = Modifier.background(Color.Black), - ) { - items(count = 100) { index -> - Button( - onClick = {}, - modifier = - Modifier.fillMaxWidth() - .transformedHeight(this, morphingTransformationSpec) - .graphicsLayer { - with(morphingTransformationSpec) { - applyContainerTransformation(scrollProgress) - } - }, - ) { - Text( - "Item $index", - modifier = - Modifier.graphicsLayer { - with(morphingTransformationSpec) { - applyContentTransformation(scrollProgress) - } - }, - ) - } - } - } -} - -@Composable -@Sampled -@Preview -fun CustomCompositingStrategyTransformationSpecSample() { - val transformationSpec = rememberTransformationSpec() - - TransformingLazyColumn( - contentPadding = PaddingValues(20.dp), - modifier = Modifier.background(Color.Black), - ) { - items(count = 100) { index -> - Button( - onClick = {}, - modifier = - Modifier.fillMaxWidth() - .transformedHeight(this, transformationSpec) - .graphicsLayer { - with(transformationSpec) { - applyContainerTransformation(scrollProgress) - } - // Using CompositingStrategy.ModulateAlpha can provide better - // performance when a container transformation involves alpha rendering, - // as it avoids an extra offscreen buffer. - // - // However, care must be taken with overlapping or transparent content - // inside the container. If the content itself uses alpha, ModulateAlpha - // can lead to multiple, incorrect alpha blending with the background - // (double-blending artifacts). - // - // To prevent this, the content's drawing layer must explicitly use - // CompositingStrategy.Offscreen to ensure internal elements are - // correctly pre-blended before the outer ModulateAlpha is applied. - compositingStrategy = ModulateAlpha - }, - ) { - Text( - text = "Item $index", - modifier = - Modifier.graphicsLayer { - // Ensure content layer uses CompositingStrategy.Offscreen when - // container uses CompositingStrategy.ModulateAlpha. - // This composition is required to guarantee correct visual blending of - // content that contains internal alpha or complex blending. - // compositingStrategy is set to CompositingStrategy.Offscreen inside - // applyContentTransformation. - with(transformationSpec) { applyContentTransformation(scrollProgress) } - }, - ) - } - } - } -} - -@Composable -@Sampled -@Preview -fun ResponsiveTransformationSpecButtonSample() { - val transformationSpec = - rememberTransformationSpec( - ResponsiveTransformationSpec.smallScreen( - // Makes the content disappear on the edges. - contentAlpha = TransformationVariableSpec(0f) - ), - ResponsiveTransformationSpec.largeScreen( - // Makes the content disappear on the edges, but a bit more aggressively. - contentAlpha = - TransformationVariableSpec(0f, transformationZoneEnterFraction = 0.2f) - ), - ) - TransformingLazyColumn( - contentPadding = PaddingValues(20.dp), - modifier = Modifier.background(Color.Black), - ) { - items(count = 100) { index -> - Button( - onClick = {}, - modifier = - Modifier.fillMaxWidth().transformedHeight(this@items, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Item $index") - } - } - } -} - -@Composable -@Sampled -@Preview -fun TransformationSpecButtonRowSample() { - // Use the spec derived from default small and large screen specs. - val transformationSpec = rememberTransformationSpec() - - TransformingLazyColumn( - contentPadding = PaddingValues(), - modifier = Modifier.background(Color.Black), - ) { - items(count = 100) { - val interactionSource1 = remember { MutableInteractionSource() } - val interactionSource2 = remember { MutableInteractionSource() } - - ButtonGroup( - modifier = - Modifier.fillMaxWidth() - .graphicsLayer { - with(transformationSpec) { - applyContainerTransformation(scrollProgress) - } - } - .transformedHeight(this, transformationSpec) - .minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - ) { - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource1), - interactionSource = interactionSource1, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - "L", - modifier = - Modifier.graphicsLayer { - with(transformationSpec) { - applyContentTransformation(scrollProgress) - } - }, - ) - } - } - Button( - onClick = {}, - modifier = Modifier.animateWidth(interactionSource2), - interactionSource = interactionSource2, - ) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - "R", - modifier = - Modifier.graphicsLayer { - with(transformationSpec) { - applyContentTransformation(scrollProgress) - } - }, - ) - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt deleted file mode 100644 index d9d2980..0000000 --- a/wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt +++ /dev/null @@ -1,89 +0,0 @@ -``` -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.items -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.ListHeader -import androidx.wear.compose.material3.MaterialTheme -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TitleCard -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.transformedHeight - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnNotificationsSample() { - val state = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - data class NotificationItem(val title: String, val body: String) - - val notifications = - listOf( - NotificationItem( - "☕ Coffee Break?", - "Step away from the screen and grab a pick-me-up. Step away from the screen and grab a pick-me-up.", - ), - NotificationItem("🌟 You're Awesome!", "Just a little reminder in case you forgot 😊"), - NotificationItem("👀 Did you know?", "Check out [app name]'s latest feature update."), - NotificationItem("📅 Appointment Time", "Your meeting with [name] is in 15 minutes."), - ) - AppScaffold { - ScreenScaffold(state) { contentPadding -> - TransformingLazyColumn(state = state, contentPadding = contentPadding) { - item { - ListHeader( - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec).animateItem(), - ) { - Text("Notifications") - } - } - items(notifications) { notification -> - TitleCard( - onClick = {}, - title = { - Text( - notification.title, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - ) - }, - subtitle = { Text(notification.body) }, - transformation = SurfaceTransformation(transformationSpec), - modifier = - Modifier.transformedHeight(this, transformationSpec).animateItem(), - ) - } - } - } - } -} -``` \ No newline at end of file diff --git a/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt b/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt deleted file mode 100644 index 379c818..0000000 --- a/wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt +++ /dev/null @@ -1,309 +0,0 @@ -``` -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.material3.samples - -import androidx.annotation.Sampled -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.foundation.lazy.TransformingLazyColumn -import androidx.wear.compose.foundation.lazy.TransformingLazyColumnFirstLayoutItemProvider -import androidx.wear.compose.foundation.lazy.itemsIndexed -import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material3.AppScaffold -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ButtonDefaults -import androidx.wear.compose.material3.Card -import androidx.wear.compose.material3.CardDefaults -import androidx.wear.compose.material3.CompactButton -import androidx.wear.compose.material3.CompactButtonDefaults -import androidx.wear.compose.material3.EdgeButton -import androidx.wear.compose.material3.ListHeader -import androidx.wear.compose.material3.ListHeaderDefaults -import androidx.wear.compose.material3.OutlinedCard -import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.SurfaceTransformation -import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.lazy.firstVisibleItemLayoutItemInfo -import androidx.wear.compose.material3.lazy.layoutItemInfoOf -import androidx.wear.compose.material3.lazy.rememberTransformationSpec -import androidx.wear.compose.material3.lazy.rememberTransformingLazyColumnFirstLayoutItemProvider -import androidx.wear.compose.material3.lazy.transformedHeight -import kotlinx.coroutines.delay - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnButtonsSample() { - val state = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - AppScaffold { - ScreenScaffold(state) { contentPadding -> - TransformingLazyColumn(state = state, contentPadding = contentPadding) { - items(count = 50) { - Button( - onClick = {}, - modifier = - Modifier.minimumVerticalContentPadding( - ButtonDefaults.minimumVerticalListContentPadding - ) - .fillMaxWidth() - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Item $it") - } - } - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnMinimumVerticalContentPaddingSample() { - val transformationSpec = rememberTransformationSpec() - val state = rememberTransformingLazyColumnState() - var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) } - var nextElement by remember { mutableIntStateOf(10) } - - fun addElementAfter(index: Int) { - elements = - elements.subList(0, index + 1) + - listOf(nextElement++) + - elements.subList(index + 1, elements.count()) - } - - fun removeElementAt(index: Int) { - elements = elements.subList(0, index) + elements.subList(index + 1, elements.count()) - } - - AppScaffold { - ScreenScaffold( - state, - edgeButton = { - EdgeButton(onClick = { elements = elements.shuffled() }) { Text("Shuffle") } - }, - ) { contentPadding -> - TransformingLazyColumn(state = state, contentPadding = contentPadding) { - itemsIndexed(elements, key = { _, key -> key }) { index, key -> - if (key % 3 == 0) { - CompactButton( - onClick = { removeElementAt(index) }, - modifier = - Modifier.minimumVerticalContentPadding( - CompactButtonDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec) - .animateItem(), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("CompactButton $key") - } - } else if (key % 4 == 0) { - ListHeader( - modifier = - Modifier.minimumVerticalContentPadding( - ListHeaderDefaults.minimumTopListContentPadding, - ListHeaderDefaults.minimumBottomListContentPadding, - ) - .transformedHeight(this, transformationSpec), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("ListHeader heading") - } - } else { - Card( - onClick = {}, - modifier = - Modifier.fillMaxWidth() - .minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec) - .animateItem(), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Card $key") - Row { - Spacer(modifier = Modifier.weight(1f)) - CompactButton( - onClick = { removeElementAt(index) }, - enabled = elements.count() > 1, - ) { - Text("-") - } - CompactButton(onClick = { addElementAfter(index) }) { Text("+") } - } - } - } - } - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnFirstLayoutItemProviderSample() { - val state = rememberTransformingLazyColumnState() - val transformationSpec = rememberTransformationSpec() - - // This sample demonstrates how to use rememberTransformingLazyColumnFirstLayoutItemProvider - // to control the expansion direction of a dynamically resizing item. By using the voice input - // card's Bottom/End edge as the layout reference, the card predictably expands upwards. - // The constant key safely falls back to default layout behavior when scrolled off-screen. - val firstLayoutItemProvider = rememberTransformingLazyColumnFirstLayoutItemProvider { - state.layoutItemInfoOf( - itemKey = "voice_input", - itemEdge = TransformingLazyColumnFirstLayoutItemProvider.ItemEdge.End, - ) - } - - var isListening by remember { mutableStateOf(false) } - var lineCount by remember { mutableIntStateOf(0) } - // Simple loop simulating text lines arriving over time - LaunchedEffect(isListening) { - if (isListening) { - while (lineCount < 5) { - delay(300) - lineCount++ - } - isListening = false - } - } - - AppScaffold { - ScreenScaffold(state) { contentPadding -> - TransformingLazyColumn( - state = state, - contentPadding = contentPadding, - firstLayoutItemProvider = firstLayoutItemProvider, - ) { - items(count = 3, key = { "message_$it" }) { index -> - Card( - onClick = {}, - modifier = - Modifier.minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec) - .animateItem(placementSpec = null), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Previous message $index") - } - } - - item(key = "voice_input") { - val voiceText = (1..lineCount).joinToString("\n") { "Voice Input line $it" } - - OutlinedCard( - onClick = { - if (lineCount >= 5) lineCount = 0 - isListening = !isListening - }, - modifier = - Modifier.minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec) - .animateItem(placementSpec = null), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text(text = voiceText.ifEmpty { "Tap to speak" }) - } - } - } - } - } -} - -@Sampled -@Preview -@Composable -fun TransformingLazyColumnFirstVisibleItemLayoutItemProviderSample() { - val transformationSpec = rememberTransformationSpec() - val state = rememberTransformingLazyColumnState() - var elements by remember { mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) } - var nextElement by remember { mutableIntStateOf(10) } - - fun addCardAfter(index: Int) { - elements = - elements.subList(0, index + 1) + - listOf(nextElement++) + - elements.subList(index + 1, elements.count()) - } - - fun removeCardAt(index: Int) { - elements = elements.subList(0, index) + elements.subList(index + 1, elements.count()) - } - - // This sample demonstrates how to use rememberTransformingLazyColumnFirstVisibleItemProvider - // to maintain a stable viewport top when items are added or removed dynamically. - // It achieves this by using the first visible item as the layout reference. - AppScaffold { - ScreenScaffold(state) { contentPadding -> - TransformingLazyColumn( - state = state, - contentPadding = contentPadding, - firstLayoutItemProvider = - rememberTransformingLazyColumnFirstLayoutItemProvider { - if (state.isScrollInProgress) null else state.firstVisibleItemLayoutItemInfo - }, - ) { - itemsIndexed(elements, key = { _, key -> key }) { index, cardKey -> - Card( - onClick = {}, - modifier = - Modifier.minimumVerticalContentPadding( - CardDefaults.minimumVerticalListContentPadding - ) - .transformedHeight(this, transformationSpec) - .animateItem(), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text("Card $cardKey") - Row { - Spacer(modifier = Modifier.weight(1f)) - CompactButton( - onClick = { removeCardAt(index) }, - enabled = elements.count() > 1, - ) { - Text("-") - } - CompactButton(onClick = { addCardAfter(index) }) { Text("+") } - } - } - } - } - } - } -} -``` \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md index c3ce954..1748d43 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/SKILL.md @@ -9,7 +9,7 @@ description: Provides guidelines for developing projected Android XR apps for di license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-07-30' + last-updated: '2026-07-22' keywords: - Jetpack Compose Glimmer - audio glasses @@ -250,7 +250,7 @@ feedback for interactive elements. If you are creating a Glimmer Icon component, read the: - **Developer Guidance:** [Jetpack Compose Glimmer: Icons](references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md) -- **API Source Code (Icon):** Use [references/icon-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source). +- **API Source Code (Icon):** Use [references/icon-source.md](references/icon-source.md). - **API Source Code (IconButton):** Use [references/iconbutton-source.md](references/iconbutton-source.md). - **API Source Code (IconSizes):** Use [references/iconsizes-source.md](references/iconsizes-source.md). @@ -264,7 +264,7 @@ different types. ##### Guidelines and usage -- **ListItem Slots:** Use the `ListItem` composable for rows. It provides predefined slots. Use [references/listitem-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source) for reference. +- **ListItem Slots:** Use the `ListItem` composable for rows. It provides predefined slots. Use [references/listitem-source.md](references/listitem-source.md) for reference. - **Visual Consistency:** When building lists of similar items, always use a consistent background color (typically `GlimmerTheme.colors.surface`) and corner radius (standard 36.dp) for every item. Don't vary these unless you are visually grouping different *types* of content. - **Integrated Title Chips:** Glimmer Lists support integrated title chips. **IF** you need a section header within a list: **THEN** enable the integrated title chip rather than adding a standalone `TitleChip` to maintain spatial consistency. - **Vertical Arrangement:** ALWAYS use `verticalArrangement = @@ -276,7 +276,7 @@ different types. If you are creating a Glimmer List component, read the: - **API Source Code (List):** Use [references/list-source.md](references/list-source.md). -- **API Source Code (ListItem):** Use [references/listitem-source.md](https://developer.android.com/agents/skills/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source). +- **API Source Code (ListItem):** Use [references/listitem-source.md](references/listitem-source.md). - **API Source Code (GlimmerLazyListState):** Use [references/liststate-source.md](references/liststate-source.md). #### Stacks diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md index 3dcc770..162b00c 100644 --- a/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md @@ -1,3 +1,6 @@ +
+ + Applicable XR devices This guidance helps you build experiences for these types of XR devices. [Learn about XR device types →](https://developer.android.com/develop/xr/devices) ![](https://developer.android.com/static/images/develop/xr/ai-glasses-icon.svg) Audio \& Display Glasses [](https://developer.android.com/develop/xr/devices#audio-display) [Learn about XR device types →](https://developer.android.com/develop/xr/devices) @@ -26,31 +29,11 @@ functionality.
-## Before requesting glasses permissions +## Register the permissions launcher -Review the [permission principles and workflows](https://developer.android.com/training/permissions/requesting#principles) to make sure you provide the -best experience for your users, like [checking whether the user has already -granted the runtime permissions](https://developer.android.com/training/permissions/requesting#already-granted) your app requires and [whether your app -should show a rationale](https://developer.android.com/training/permissions/requesting#explain) to the user before requesting glasses-specific -permissions. - -## Permission scenarios - -There are different scenarios you might encounter when requesting [dangerous -runtime permissions](https://developer.android.com/guide/topics/permissions/requesting#normal-dangerous) on audio and display glasses: - -- Request runtime permissions from a projected activity -- Request runtime permissions from a phone activity - -See the following sections in this guide for details about each scenario. - -### Request and handle runtime permissions from a projected activity - -To request permissions for audio glasses and display glasses, first use the -[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the -[`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method to register the permissions -launcher. When the user has acted on the permission request, the callback -receives a map of permission names to their granted status. +To request permissions for audio glasses and display glasses, first you use the +[`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) with the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) +method to register the permissions launcher. ```kotlin @@ -69,18 +52,15 @@ private val requestPermissionLauncher: ActivityResultLauncher -#### Key points about the code +### Key points about the code +- The code creates an [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method. The callback receives a map of permission names to their granted status. - You need to specify which permissions your app requires, such as [`Manifest.permission.CAMERA`](https://developer.android.com/reference/kotlin/android/Manifest.permission#camera) or [`Manifest.permission.RECORD_AUDIO`](https://developer.android.com/reference/kotlin/android/Manifest.permission#record_audio). -- Your app should handle both granted and denied results gracefully in the launcher's callback. -To trigger the permission request flow, pass a list of -[`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) to your registered permission -launcher's `launch` method. The `ProjectedPermissionsRequestParams` object -bundles the requested manifest permissions together with a custom `rationale` -string. The `rationale` string must clearly and concisely explain why the app -requires access to the glasses' hardware features (such as the camera or -microphone). +## Create the request function + +Next, you'll create a function that uses your app's permissions launcher to +request the permissions from the user at runtime. ```kotlin @@ -95,17 +75,72 @@ private fun requestHardwarePermissions() {
-#### Key points about the code +### Key points about the code +- The `requestHardwarePermissions` function builds a [`ProjectedPermissionsRequestParams`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsRequestParams) object. This object bundles the list of permissions your app needs and the user-facing rationale. Make the rationale clear and concise to explain why your app needs these permissions. - Calling `launch` on the launcher triggers the [permission request user flow](https://developer.android.com/develop/xr/jetpack-xr-sdk/request-hardware-permissions#permissions-user-flow). +- Your app should handle both granted and denied results gracefully in the launcher's callback. -#### Understand the permission request user flow +## Create the permissions check function + +Next, you'll create a function that can check whether the user has granted +permissions to your app. + + +```kotlin +private fun hasCameraPermission(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED +} +``` + +
+ +## Add the permission request logic + +And lastly, create the logic that uses these functions to check for and request +the permissions at runtime. + + +```kotlin +if (hasCameraPermission()) { + initializeGlassesFeatures() +} else { + requestHardwarePermissions() +} +``` + +
+ +### Key points about the code + +- If the user has already granted your app the required permissions, the `initializeGlassesFeatures` function is called to initialize your app's experience. This function is defined as [part of your app's activity for AI + glasses](https://developer.android.com/develop/xr/jetpack-xr-sdk/glasses/first-activity#create-activity). + +## Understand the permission request user flow When you launch a permission request using the [`ProjectedPermissionsResultContract`](https://developer.android.com/reference/kotlin/androidx/xr/projected/permissions/ProjectedPermissionsResultContract#ProjectedPermissionsResultContract()) method, the system initiates a coordinated user flow across both the glasses and the phone. +
+ +> [!IMPORTANT] +> **Important:** You should call the `ProjectedPermissionsResultContract` method from an [`Activity`](https://developer.android.com/reference/kotlin/android/app/Activity) displayed on the glasses. Don't use the standard Android permission APIs (such as [`requestPermissions`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat#requestPermissions(android.app.Activity,%20java.lang.String%5B%5D,%20int)) with [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher)) in code running on the glasses. Doing so attempts to launch a non-interactable permission dialog on the glasses, breaking the user flow. +> +>
+> +> If your app already has an `Activity` displayed on the phone, you should use +> [`Activity#requestPermissions(permissions, requestCode, deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1), where +> the `deviceId` comes from calling the [`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) method on the +> [`Context`](https://developer.android.com/reference/kotlin/android/content/Context) returned by calling +> [`ProjectedContext.createProjectedDeviceContext`](https://developer.android.com/reference/kotlin/androidx/xr/projected/ProjectedContext#createProjectedDeviceContext(android.content.Context)). +> +>
+> +
+ During the permissions user flow, here is what your app and the user can expect: 1. **On the glasses** : An activity appears on the **projected device @@ -139,75 +174,5 @@ During the permissions user flow, here is what your app and the user can expect: 4. **Receiving the result** : After the user makes their final choice, the activities on both the phone and glasses are dismissed. Your - [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map - containing the granted status for each requested permission. - -### Request runtime permissions in a phone activity - -If your app is running in a phone activity but requires permissions for audio or -display glasses---for example, to let a user switch a video stream to the glasses' -camera for a first-person point of view---request the permissions using the -device-specific [`Activity#requestPermissions(permissions, requestCode, -deviceId)`](https://developer.android.com/reference/kotlin/android/app/Activity#requestpermissions_1) method. - -To target audio and display glasses, obtain the appropriate device ID by calling -[`getDeviceId`](https://developer.android.com/reference/kotlin/android/content/Context#getdeviceid) on a projected device context. Pass this ID when requesting -permissions from your phone activity, as shown in the following example: - - -```kotlin -// Request the projected permission from phone activity -requestPermissions( - arrayOf(Manifest.permission.CAMERA), - // REQUEST_CODE_GLASSES_CAMERA is a developer-defined constant - REQUEST_CODE_GLASSES_CAMERA, - projectedDeviceId -) -``` - -
- -#### Handle the permission results - -Once the user responds to the permission dialog on the phone, their decision is -delivered to your app by invoking the onRequestPermissionsResult callback. - -To handle the response, override [`onRequestPermissionsResult`](https://developer.android.com/reference/kotlin/androidx/core/app/ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int,java.lang.String%5B%5D,int%5B%5D)) within the -`Activity` instance that initiated the permission request. - -The following code snippet shows how to handle the callback and verify whether -the user granted the requested permissions: - - -```kotlin -private companion object { - // REQUEST_CODE_GLASSES_CAMERA is a developer-defined constant. - const val REQUEST_CODE_GLASSES_CAMERA = 1001 -} - -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray, - deviceId: Int -) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults, deviceId) - - // Handle the result of the permission request - if (requestCode == REQUEST_CODE_GLASSES_CAMERA && deviceId == projectedDeviceId) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Proceed with glasses camera features - } else { - // Handle glasses permission denied - } - } -} -``` - -
- -##### Key points about the code - -- Use the `deviceId` overload with the `onRequestPermissionsResult` callback to ensure the permission status is correctly mapped to the specific context, such as the audio or display glasses rather than the host phone. -- Your app should handle both granted and denied results gracefully. \ No newline at end of file + [`ActivityResultLauncher`](https://developer.android.com/reference/kotlin/androidx/activity/result/ActivityResultLauncher) callback is then invoked with a map containing + the granted status for each requested permission. \ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md new file mode 100644 index 0000000..1b8ad16 --- /dev/null +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/icon-source.md @@ -0,0 +1,389 @@ +When creating a Glimmer Icon component, refer to the following source code in +`Icon.kt`: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.structuralEqualityPolicy +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.CacheDrawModifierNode +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toolingGraphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp + +/** + * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For + * generic images that should not be tinted, and should not use the provided icon size, use the + * generic [androidx.compose.foundation.Image] instead. + * + * @param imageVector [ImageVector] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + imageVector: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = rememberVectorPainter(imageVector), + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a + * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so + * that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. Use the other overload of Icon without a [tint] parameter to apply the + * recommended content color provided by a [surface]. For multicolored icons and icons that should + * not be tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, + * and should not use the provided icon size, use the generic [androidx.compose.foundation.Image] + * instead. + * + * @param imageVector [ImageVector] to draw inside this icon + * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no tint + * is applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + imageVector: ImageVector, + tint: Color, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = rememberVectorPainter(imageVector), + tint = + if (tint.isSpecified) { + { tint } + } else null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For + * generic images that should not be tinted, and should not use the provided icon size, use the + * generic [androidx.compose.foundation.Image] instead. + * + * @param bitmap [ImageBitmap] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon(bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier) { + val painter = remember(bitmap) { BitmapPainter(bitmap) } + Icon(painter = painter, contentDescription = contentDescription, modifier = modifier) +} + +/** + * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a tint + * of [tint]. Icon is an opinionated component designed to be used with single-color icons so that + * they can be tinted correctly for the component they are placed in. The recommended icon sizes can + * be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly using [size], + * and components can use [LocalIconSize] to set the preferred size for any Icons inside the + * component. Use the other overload of Icon without a [tint] parameter to apply the recommended + * content color provided by a [surface]. For multicolored icons and icons that should not be + * tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, and + * should not use the provided icon size, use the generic [androidx.compose.foundation.Image] + * instead. + * + * @param bitmap [ImageBitmap] to draw inside this icon + * @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no tint is + * applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + bitmap: ImageBitmap, + tint: Color, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + val painter = remember(bitmap) { BitmapPainter(bitmap) } + Icon( + painter = painter, + tint = + if (tint.isSpecified) { + { tint } + } else null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a + * content color tint. Icon is an opinionated component designed to be used with single-color icons + * so that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. The content color used to tint this icon is provided by [surface]. To set a + * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons + * that should not be tinted, use the overload and set `null` for the tint parameter. For generic + * images that should not be tinted, and should not use the provided icon size, use the generic + * [androidx.compose.foundation.Image] instead. + * + * @param painter [Painter] to draw inside this icon + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon(painter: Painter, contentDescription: String?, modifier: Modifier = Modifier) { + Icon( + painter = painter, + useContentColor = true, + tint = null, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a + * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so + * that they can be tinted correctly for the component they are placed in. The recommended icon + * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly + * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons + * inside the component. Use the other overload of Icon without a [tint] parameter to apply the + * recommended content color provided by a [surface]. For multicolored icons and icons that should + * not be tinted, set [tint] to `null`. For generic images that should not be tinted, and should not + * use the provided icon size, use the generic [androidx.compose.foundation.Image] instead. + * + * @param painter [Painter] to draw inside this icon + * @param tint tint to be applied to [painter]. If null, then no tint is applied. + * @param contentDescription text used by accessibility services to describe what this icon + * represents. This should always be provided unless this icon is used for decorative purposes, + * and does not represent a meaningful action that a user can take. This text should be localized, + * such as by using [androidx.compose.ui.res.stringResource] or similar + * @param modifier the [Modifier] to be applied to this icon + * @see LocalIconSize + * @see GlimmerTheme.Companion.iconSizes + */ +@Composable +public fun Icon( + painter: Painter, + tint: ColorProducer?, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + Icon( + painter = painter, + useContentColor = false, + tint = tint, + contentDescription = contentDescription, + modifier = modifier, + ) +} + +/** + * CompositionLocal containing the preferred size of an icon. This value will be used by [Icon] by + * default - it can be overridden with a [size] modifier. + */ +public val LocalIconSize: ProvidableCompositionLocal = + compositionLocalOf(structuralEqualityPolicy()) { GlimmerTheme._iconSizes.medium } + +@Composable +private fun Icon( + painter: Painter, + useContentColor: Boolean, + tint: ColorProducer?, + contentDescription: String?, + modifier: Modifier, +) { + val colorFilter = + if (useContentColor || tint != null) { + IconColorFilterElement( + useContentColor = useContentColor, + tint = tint, + painter = painter, + ) + } else { + Modifier + } + val semantics = + if (contentDescription != null) { + Modifier.semantics { + this.contentDescription = contentDescription + this.role = Role.Image + } + } else { + Modifier + } + Box( + modifier + .toolingGraphicsLayer() + .size(LocalIconSize.current) + .then(colorFilter) + .paint(painter, contentScale = ContentScale.Fit) + .then(semantics) + ) +} + +@Suppress("ModifierNodeInspectableProperties") +private class IconColorFilterElement( + private val useContentColor: Boolean, + private val tint: ColorProducer?, + private val painter: Painter, +) : ModifierNodeElement() { + override fun create() = IconColorFilterNode(useContentColor, tint, painter) + + override fun update(node: IconColorFilterNode) { + node.update(useContentColor, tint, painter) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IconColorFilterElement) return false + + if (useContentColor != other.useContentColor) return false + if (tint != other.tint) return false + if (painter != other.painter) return false + + return true + } + + override fun hashCode(): Int { + var result = useContentColor.hashCode() + result = 31 * result + (tint?.hashCode() ?: 0) + result = 31 * result + painter.hashCode() + return result + } +} + +private class IconColorFilterNode( + private var useContentColor: Boolean, + private var tint: ColorProducer?, + private var painter: Painter, +) : DelegatingNode() { + val cacheDrawNode = + delegate( + CacheDrawModifierNode { + val layer = obtainGraphicsLayer() + layer.apply { record { drawContent() } } + var cachedTintColor = Color.Unspecified + var cachedColorFilter: ColorFilter? = null + onDrawWithContent { + val tintColor = + if (useContentColor) { + currentContentColor() + } else { + tint?.invoke() ?: Color.Unspecified + } + if (cachedTintColor != tintColor) { + cachedTintColor = tintColor + cachedColorFilter = + if (tintColor.isSpecified) ColorFilter.tint(tintColor) else null + layer.colorFilter = cachedColorFilter + } + drawLayer(graphicsLayer = layer) + } + } + ) + + fun update(useContentColor: Boolean, tint: ColorProducer?, painter: Painter) { + val painterChanged = this.painter != painter + if (this.useContentColor != useContentColor || this.tint != tint || painterChanged) { + this.useContentColor = useContentColor + this.tint = tint + this.painter = painter + if (painterChanged) { + cacheDrawNode.invalidateDrawCache() + } else { + cacheDrawNode.invalidateDraw() + } + } + } +} +``` + +
\ No newline at end of file diff --git a/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md new file mode 100644 index 0000000..0f199a2 --- /dev/null +++ b/xr/display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md @@ -0,0 +1,281 @@ +When creating a Glimmer Icon Sizes component, refer to the following source +code in `ListItem.kt` for list items: + + +```kotlin +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.xr.glimmer + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * ListItem is a component used to represent a single item in a + * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may + * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * label is displayed below the primary label and can be used to provide additional information. A + * ListItem fills the maximum width available by default. + * + * This ListItem is focusable - see the other [ListItem] overload for a clickable ListItem. + * + * A simple ListItem with just a primary label: + * + * @sample androidx.xr.glimmer.samples.ListItemSample + * + * A ListItem with a primary and supporting label: + * + * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelSample + * + * A ListItem with a primary label, a supporting label, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.ListItemWithSupportingLabelAndLeadingIconSample + * @param modifier the [Modifier] to be applied to this list item + * @param supportingLabel optional supporting label to be placed underneath the primary label + * [content] + * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is + * typically an [Icon] tinted with [contentColor] by default. + * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This + * is typically an [Icon] tinted with [contentColor] by default. + * @param shape the [Shape] used to clip this list item, and also used to draw the background and + * border + * @param color background color of this list item + * @param contentColor content color used by components inside [content], [supportingLabel], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this list item + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this list item. You can use this to change the list item's appearance + * or preview the list item in different states. Note that if `null` is provided, interactions + * will still happen internally. + * @param content the main content / primary label to display inside this list item + */ +@Composable +public fun ListItem( + modifier: Modifier = Modifier, + supportingLabel: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = ListItemDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + ListItemImpl( + modifier = modifier, + onClick = null, + supportingLabel = supportingLabel, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +/** + * ListItem is a component used to represent a single item in a + * [androidx.xr.glimmer.list.GlimmerLazyColumn]. A ListItem has a primary label [content], and may + * also have any combination of [supportingLabel], [leadingIcon], and [trailingIcon]. The supporting + * label is displayed below the primary label and can be used to provide additional information. A + * ListItem fills the maximum width available by default. + * + * This ListItem is focusable and clickable - see the other [ListItem] overload for a ListItem that + * is only focusable. + * + * A simple clickable ListItem with just a primary label: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemSample + * + * A clickable ListItem with a primary and supporting label: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelSample + * + * A clickable ListItem with a primary label, a supporting label, and a leading icon: + * + * @sample androidx.xr.glimmer.samples.ClickableListItemWithSupportingLabelAndLeadingIconSample + * @param onClick called when this list item is clicked + * @param modifier the [Modifier] to be applied to this list item + * @param supportingLabel optional supporting label to be placed underneath the primary label + * [content] + * @param leadingIcon optional leading icon to be placed before the primary label [content]. This is + * typically an [Icon] tinted with [contentColor] by default. + * @param trailingIcon optional trailing icon to be placed after the primary label [content]. This + * is typically an [Icon] tinted with [contentColor] by default. + * @param shape the [Shape] used to clip this list item, and also used to draw the background and + * border + * @param color background color of this list item + * @param contentColor content color used by components inside [content], [supportingLabel], + * [leadingIcon], and [trailingIcon]. + * @param border the border to draw around this list item + * @param contentPadding the spacing values to apply internally between the container and the + * content + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting Interactions for this list item. You can use this to change the list item's appearance + * or preview the list item in different states. Note that if `null` is provided, interactions + * will still happen internally. + * @param content the main content / primary label to display inside this list item + */ +@Composable +public fun ListItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, + supportingLabel: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = GlimmerTheme.shapes.medium, + color: Color = GlimmerTheme.colors.surface, + contentColor: Color = calculateContentColor(color), + border: BorderStroke? = SurfaceDefaults.border(), + contentPadding: PaddingValues = ListItemDefaults.contentPadding, + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit, +) { + ListItemImpl( + modifier = modifier, + onClick = onClick, + supportingLabel = supportingLabel, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +@Composable +private fun ListItemImpl( + modifier: Modifier, + onClick: (() -> Unit)?, + supportingLabel: @Composable (() -> Unit)?, + leadingIcon: @Composable (() -> Unit)?, + trailingIcon: @Composable (() -> Unit)?, + shape: Shape, + color: Color, + contentColor: Color, + border: BorderStroke?, + contentPadding: PaddingValues, + interactionSource: MutableInteractionSource?, + content: @Composable () -> Unit, +) { + val iconSize = GlimmerTheme.iconSizes.large + val typography = GlimmerTheme.typography + val innerPadding = GlimmerTheme.componentSpacingValues.small + val internalInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + + val surfaceModifier = + Modifier.surface( + shape = shape, + color = color, + contentColor = contentColor, + // TODO(b/532516157): Reenable depthEffect after b/446294492 is fixed. + depthEffect = null, + border = border, + interactionSource = internalInteractionSource, + ) + .then( + if (onClick != null) { + Modifier.clickable( + interactionSource = internalInteractionSource, + onClick = onClick, + ) + } else { + Modifier.focusable(interactionSource = internalInteractionSource) + } + ) + + Row( + modifier = + modifier + .then(surfaceModifier) + .fillMaxWidth() + .defaultMinSize(minHeight = MinimumHeight) + .padding(contentPadding), + verticalAlignment = CenterVertically, + ) { + if (leadingIcon != null) { + Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopStart) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = leadingIcon) + } + } + Column(Modifier.weight(1f).padding(horizontal = innerPadding)) { + if (supportingLabel == null) { + CompositionLocalProvider( + LocalTextStyle provides typography.bodySmall, + content = content, + ) + } else { + CompositionLocalProvider( + LocalTextStyle provides typography.titleSmall, + content = content, + ) + CompositionLocalProvider( + LocalTextStyle provides typography.bodySmall, + content = supportingLabel, + ) + } + } + if (trailingIcon != null) { + Box(modifier = Modifier.align(Alignment.Top), contentAlignment = Alignment.TopEnd) { + CompositionLocalProvider(LocalIconSize provides iconSize, content = trailingIcon) + } + } + } +} + +/** Default values used for [ListItem] */ +public object ListItemDefaults { + /** Default content padding used for a [ListItem] */ + public val contentPadding: PaddingValues + @Composable get() = PaddingValues(GlimmerTheme.componentSpacingValues.large) +} + +/** Default minimum height for a [ListItem] */ +private val MinimumHeight = 80.dp +``` + +
\ No newline at end of file From 7311f4905d9c7553abdccfc974656516dff3d4ab Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Fri, 7 Aug 2026 14:24:00 +0000 Subject: [PATCH 41/61] Updates skills (2026-08-07 14:24) --- .claude-plugin/marketplace.json | 2 + .codex-plugin/plugin.json | 10 + 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 | 316 ++------ .../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 | 26 +- device-ai/appfunctions/context.md | 26 - .../references/adb-interaction-testing.md | 35 +- device-ai/appfunctions/references/context.md | 11 + .../references/feature-discovery-analysis.md | 33 +- .../implementation-configuration.md | 53 +- .../kdoc-refinement-optimization.md | 16 +- .../migrate-to-service-entry-point.md | 71 +- devtools/android-cli/SKILL.md | 123 ++- identity/verified-email/SKILL.md | 116 +-- .../identity/credential-manager/index.md | 35 +- .../credential-verifier.md | 27 +- .../email-verification-implementation.md | 106 +-- .../digital-credentials/email-verification.md | 78 +- .../identity/digital-credentials/index.md | 60 +- .../identity/passkeys/create-passkeys.md | 121 +-- .../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 | 94 +-- .../layouts/adaptive/grid/get-started.md | 13 +- .../ui/compose/layouts/adaptive/grid/index.md | 40 +- .../layouts/adaptive/grid/item-properties.md | 58 +- .../layouts/adaptive/mediaquery/index.md | 138 +--- .../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 | 224 ++++++ .../media/media3/cast/create-castplayer.md | 367 +++++++++ .../media3/cast/customize-castoptions.md | 88 +++ .../android/media/media3/cast/index.md | 13 + navigation/navigation-3/SKILL.md | 13 +- .../guide/navigation/navigation-3/index.md | 34 +- .../navigation-3/migration-guide.md | 165 ++-- .../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 | 184 +++++ .../navigation-3/recipes/conditional.md | 2 + .../navigation-3/recipes/deeplinks-basic.md | 744 ------------------ .../recipes/deeplinks-custommatcher.md | 178 +++++ .../recipes/deeplinks-staticuri.md | 143 ++++ ...ced.md => deeplinks-syntheticbackstack.md} | 68 +- .../recipes/deeplinks-uriarguments.md | 396 ++++++++++ .../navigation/navigation-3/recipes/dialog.md | 2 + .../navigation-3/recipes/lifecycle-owner.md | 187 +++++ .../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 | 74 +- .../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 | 38 +- .../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 | 119 +-- .../testing/other-components/ui-automator.md | 106 +-- play/engage-sdk-integration/SKILL.md | 18 +- .../android/guide/playcore/engage/faq.md | 215 ++--- .../android/guide/playcore/engage/food.md | 238 ++---- .../guide/playcore/engage/healthandfitness.md | 248 ++---- .../android/guide/playcore/engage/listen.md | 226 ++---- .../guide/playcore/engage/otherverticals.md | 280 ++----- .../android/guide/playcore/engage/read.md | 209 ++--- .../android/guide/playcore/engage/shopping.md | 269 ++----- .../android/guide/playcore/engage/social.md | 649 +++++++++++++++ .../android/guide/playcore/engage/travel.md | 293 ++----- .../engage/tv/continue-watching/index.md | 15 +- .../guide/playcore/engage/tv/entitlements.md | 101 +-- .../playcore/engage/tv/getting-started.md | 51 +- .../playcore/engage/tv/recommendations.md | 114 +-- .../android/guide/playcore/engage/watch.md | 220 ++---- .../references/patterns.md | 48 +- .../references/requests.md | 3 +- .../SKILL.md | 22 +- .../google/play/billing/release-notes.md | 559 +++---------- .../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 | 692 ++++++++++++++++ .../training/tv/get-started/navigation.md | 246 ++++++ .../training/tv/playback/compose/browse.md | 149 ++++ .../training/tv/playback/compose/index.md | 128 +++ wear/wear-compose-m3/SKILL.md | 184 +++-- .../wearables/compose/migrate-to-material3.md | 170 ++-- .../foundation/AmbientModeSample.kt.md.txt | 123 +++ .../foundation/CurvedWorldSample.kt.md.txt | 430 ++++++++++ .../foundation/ExpandableSample.kt.md.txt | 99 +++ .../HierarchicalFocusSample.kt.md.txt | 141 ++++ .../foundation/PagerSamples.kt.md.txt | 56 ++ .../foundation/RotarySamples.kt.md.txt | 153 ++++ .../ScalingLazyColumnSample.kt.md.txt | 131 +++ .../SwipeToDismissBoxSample.kt.md.txt | 168 ++++ .../foundation/SwipeToRevealSample.kt.md.txt | 282 +++++++ .../TransformingLazyColumnSample.kt.md.txt | 335 ++++++++ .../material3/AlertDialogSample.kt.md.txt | 377 +++++++++ .../material3/AnimatedTextSample.kt.md.txt | 165 ++++ .../material3/ButtonGroupSample.kt.md.txt | 91 +++ .../material3/ButtonSample.kt.md.txt | 496 ++++++++++++ .../references/material3/CardSample.kt.md.txt | 470 +++++++++++ .../material3/CheckboxButtonSample.kt.md.txt | 69 ++ .../ConfirmationDialogSample.kt.md.txt | 162 ++++ .../material3/CurvedTextSamples.kt.md.txt | 78 ++ .../material3/DatePickerSample.kt.md.txt | 131 +++ .../DynamicColorSchemeSample.kt.md.txt | 74 ++ .../material3/EdgeButtonSample.kt.md.txt | 63 ++ .../FadingExpandingLabelSample.kt.md.txt | 60 ++ .../material3/IconButtonSample.kt.md.txt | 125 +++ .../IconToggleButtonSample.kt.md.txt | 116 +++ .../material3/LevelIndicatorSample.kt.md.txt | 68 ++ .../LinearProgressIndicatorSample.kt.md.txt | 50 ++ .../material3/ListHeaderSample.kt.md.txt | 177 +++++ .../OneHandedGestureSamples.kt.md.txt | 651 +++++++++++++++ .../OpenOnPhoneDialogSample.kt.md.txt | 57 ++ .../material3/PageIndicatorSample.kt.md.txt | 110 +++ .../material3/PagerScaffoldSample.kt.md.txt | 204 +++++ .../material3/PickerGroupSample.kt.md.txt | 139 ++++ .../material3/PickerSample.kt.md.txt | 111 +++ .../material3/PlaceholderSample.kt.md.txt | 182 +++++ .../ProgressIndicatorSample.kt.md.txt | 263 +++++++ .../material3/RadioButtonSample.kt.md.txt | 109 +++ .../material3/ScaffoldSample.kt.md.txt | 146 ++++ .../material3/ScrollAwaySample.kt.md.txt | 97 +++ .../material3/ScrollIndicatorSample.kt.md.txt | 66 ++ .../material3/SliderSample.kt.md.txt | 86 ++ .../material3/StepperSample.kt.md.txt | 148 ++++ .../SurfaceTransformationSample.kt.md.txt | 165 ++++ .../SwipeToDismissBoxSample.kt.md.txt | 171 ++++ .../material3/SwipeToRevealSample.kt.md.txt | 364 +++++++++ .../material3/SwitchButtonSample.kt.md.txt | 68 ++ .../material3/TextButtonSample.kt.md.txt | 115 +++ .../TextToggleButtonSample.kt.md.txt | 71 ++ .../material3/TimePickerSample.kt.md.txt | 151 ++++ .../material3/TimeTextSample.kt.md.txt | 65 ++ .../TransformationSpecSample.kt.md.txt | 246 ++++++ ...ingLazyColumnNotificationsSample.kt.md.txt | 89 +++ .../TransformingLazyColumnSample.kt.md.txt | 309 ++++++++ .../SKILL.md | 99 +-- .../ai-glasses/guides/interaction/inputs.md | 16 +- .../access-hardware-projected-context.md | 311 ++++---- .../jetpack-compose-glimmer/buttons.md | 29 +- .../jetpack-compose-glimmer/cards.md | 23 +- .../jetpack-compose-glimmer/focus.md | 49 +- .../jetpack-compose-glimmer/icons.md | 20 +- .../jetpack-compose-glimmer/text.md | 25 +- .../jetpack-compose-glimmer/title-chips.md | 17 +- .../request-hardware-permissions.md | 196 ++--- .../references/button-samples-source.md | 6 +- .../references/button-source.md | 6 +- .../references/card-samples-source.md | 6 +- .../references/card-source.md | 6 +- .../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 | 6 +- .../references/iconsizes-source.md | 6 +- .../references/list-source.md | 6 +- .../references/listitem-source.md | 6 +- .../references/liststate-source.md | 6 +- .../references/material-hct-source.md | 2 + .../references/projectedcontext-source.md | 6 +- .../references/stack-source.md | 6 +- .../references/stackitemscope-source.md | 6 +- .../references/stackstate-source.md | 6 +- .../references/surface-source.md | 6 +- .../references/titlechip-samples-source.md | 6 +- .../references/titlechip-source.md | 6 +- 224 files changed, 15705 insertions(+), 6829 deletions(-) delete mode 100644 device-ai/appfunctions/context.md create mode 100644 device-ai/appfunctions/references/context.md create mode 100644 media/media3-cast-integration/SKILL.md create mode 100644 media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md create mode 100644 media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md create mode 100644 media/media3-cast-integration/references/android/media/media3/cast/index.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md delete mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md rename navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/{deeplinks-advanced.md => deeplinks-syntheticbackstack.md} (62%) create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md create mode 100644 navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md create mode 100644 play/engage-sdk-integration/references/android/guide/playcore/engage/social.md create mode 100644 tv/leanback-to-compose-tv-migration/SKILL.md create mode 100644 tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md create mode 100644 tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/browse.md create mode 100644 tv/leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md create mode 100644 wear/wear-compose-m3/references/foundation/AmbientModeSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/PagerSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/RotarySamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CardSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/DatePickerSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/IconButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PickerGroupSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PickerSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SliderSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/StepperSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SwipeToRevealSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TextButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TimePickerSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TimeTextSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt create mode 100644 wear/wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d8900b3..ba1297f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -22,6 +22,7 @@ "./jetpack-compose/adaptive", "./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose", "./jetpack-compose/theming/styles", + "./media/media3-cast-integration", "./navigation/navigation-3", "./performance/r8-analyzer", "./play/engage-sdk-integration", @@ -32,6 +33,7 @@ "./security/android-intent-security", "./system/edge-to-edge", "./testing/testing-setup", + "./tv/leanback-to-compose-tv-migration", "./wear/wear-compose-m3", "./xr/display-glasses-with-jetpack-compose-glimmer" ], diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f28a37b..13f515a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -55,6 +55,11 @@ "path": "./jetpack-compose/theming/styles" } }, + { + "source": { + "path": "./media/media3-cast-integration" + } + }, { "source": { "path": "./navigation/navigation-3" @@ -105,6 +110,11 @@ "path": "./testing/testing-setup" } }, + { + "source": { + "path": "./tv/leanback-to-compose-tv-migration" + } + }, { "source": { "path": "./wear/wear-compose-m3" diff --git a/build-system/agp/agp-9-upgrade/SKILL.md b/build-system/agp/agp-9-upgrade/SKILL.md index 247b6ee..8ad57ba 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-07-23' + last-updated: '2026-08-06' keywords: - Android Gradle Plugin 9 - AGP 9 @@ -18,23 +18,13 @@ 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. @@ -44,8 +34,7 @@ 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. @@ -57,19 +46,15 @@ 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 @@ -99,5 +84,4 @@ 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 7306e7b..820bf0f 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,33 +1,20 @@ -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. @@ -37,8 +24,7 @@ message: 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). @@ -50,10 +36,7 @@ To resolve this error, follow these steps: ### 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 @@ -137,8 +120,7 @@ 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] @@ -147,16 +129,11 @@ 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] @@ -216,8 +193,7 @@ 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: @@ -274,15 +250,9 @@ 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 @@ -322,10 +292,7 @@ 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 @@ -347,57 +314,36 @@ 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 d092353..40a5d58 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,25 +1,16 @@
-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/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 | |---:|:---:|:---:|:---:| @@ -32,52 +23,25 @@ Here is other compatibility info: ## 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 | |---|---|---| @@ -90,73 +54,43 @@ using various deprecated APIs, including: | `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 { @@ -170,13 +104,9 @@ file: ### 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 { @@ -199,17 +129,13 @@ 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 @@ -220,8 +146,7 @@ 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 | |---|---|---|---| @@ -236,7 +161,7 @@ This gives you the choice to preserve the AGP 8.13 behavior when upgrading: | `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`. | @@ -260,16 +185,9 @@ 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** @@ -295,8 +213,7 @@ 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 @@ -304,50 +221,37 @@ 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()) @@ -357,43 +261,25 @@ 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 | |---|---|---| @@ -404,8 +290,7 @@ build. 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 | |---|---| @@ -418,11 +303,7 @@ 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); @@ -442,118 +323,73 @@ compiler: 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 ### Android Gradle plugin 9.0.0 -| Fixed Issues || -|---|---| -| **Android Gradle Plugin** | |---| | [Issue #171293712](https://issuetracker.google.com/issues/171293712) Feature Request: Inject ideal AGP version as a property | | [Issue #443976533](https://issuetracker.google.com/issues/443976533) Stabilize SingleArtifact.VERSION_CONTROL_INFO_FILE | | [Issue #223643506](https://issuetracker.google.com/issues/223643506) androidTest connectedCheck logcat output is broken | | [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets | | [Issue #460094802](https://issuetracker.google.com/issues/460094802) missingDimensionStrategy prefers a flavor maching its own name even from an unrelated dimension | | [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets | | [Issue #471410336](https://issuetracker.google.com/issues/471410336) AGP 9.0.0-rc01 doesn't resolve Kotlin libraries via kotlin() function | | [Issue #450851465](https://issuetracker.google.com/issues/450851465) Built-in-kotlin does not publish kotlin-stdlib dependency constraint in maven pom | | [Issue #420592288](https://issuetracker.google.com/issues/420592288) Add a test case for divergence between compileSdk and targetSdk | | [Issue #449153004](https://issuetracker.google.com/issues/449153004) empty resConfigs value leads to obscure aapt error | | [Issue #447375921](https://issuetracker.google.com/issues/447375921) Pure Java project have dependency on kotlin stdlib. | | [Issue #368600704](https://issuetracker.google.com/issues/368600704) Remove deprecated KotlinMultiplatformAndroidCompilationBuilder properties in AGP 9.0 | | [Issue #445209309](https://issuetracker.google.com/issues/445209309) \`com.android.tools.build:gradle:9.0.0-alpha05\` should have an api dependency on KGP and gradle-api | | [Issue #452645779](https://issuetracker.google.com/issues/452645779) Rename com.android.experimental.built-in-kotlin Gradle plugin | | [Issue #446220448](https://issuetracker.google.com/issues/446220448) AGP9: \`variant.sources.kotlin!!.addGeneratedSourceDirectory()\` is not working | | [Issue #448450771](https://issuetracker.google.com/issues/448450771) Aar metadata checks on compile Sdk uses the old DSL | | [Issue #441523448](https://issuetracker.google.com/issues/441523448) Remove deprecated \`com.android.build.api.dsl.ManagedDevices.devices\` property | | [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets | | [Issue #433758231](https://issuetracker.google.com/issues/433758231) Fail android library publishing if consumer keep file contains -dontobfuscate | | [Issue #241955408](https://issuetracker.google.com/issues/241955408) No Options to Print Mapping for Optimized Resources | | [Issue #436595826](https://issuetracker.google.com/issues/436595826) Make it an error to call finalizeDsl after this phase has been passed | | [Issue #457089670](https://issuetracker.google.com/issues/457089670) AGP initializes jetifier config even when jetifier is disabled | | [Issue #452246814](https://issuetracker.google.com/issues/452246814) built in kotlin does not add kotlinStdlib as a compile time dependency when \`kotlin.stdlib.default.dependency\` is true to module and pom files | | [Issue #442250902](https://issuetracker.google.com/issues/442250902) New optimizaation DSL does not create configuration.txt by default | | [Issue #443587266](https://issuetracker.google.com/issues/443587266) AGP 8.13.0 fails to verify nav graph in a module | | [Issue #444260628](https://issuetracker.google.com/issues/444260628) AGP uses deprecated Gradle API: multi-string notation | | [Issue #347732357](https://issuetracker.google.com/issues/347732357) Warn users trying to use legacy multidex library with minSdkVersion \>=21 | | [Issue #333831734](https://issuetracker.google.com/issues/333831734) A build fails if there are code generation tasks | | [Issue #446123111](https://issuetracker.google.com/issues/446123111) With \`android.builtInKotlin=false\` and \`android.newDsl=false\` and \`android.enableLegacyVariantApi=false\`, using \`kotlin-android\` plugin will fail with "API 'applicationVariants' is obsolete" | | [Issue #443037365](https://issuetracker.google.com/issues/443037365) Built-in Kotlin fails to resolve unversioned kotlin-stdlib when kotlin.stdlib.default.dependency=false | | [Issue #445967244](https://issuetracker.google.com/issues/445967244) DexData opens a file without closing, preventing cleanup | | [Issue #368609737](https://issuetracker.google.com/issues/368609737) AndroidSourceDirectorySet should stop extending PatternFilterable in AGP 9.0 | | [Issue #389707041](https://issuetracker.google.com/issues/389707041) Test Fixture Error in test only modules | | [Issue #353249347](https://issuetracker.google.com/issues/353249347) Incorrect error when using context receivers in test fixtures | | [Issue #351046197](https://issuetracker.google.com/issues/351046197) Incorrect IDE errors for Kotlin code in testFixtures | | [Issue #446889652](https://issuetracker.google.com/issues/446889652) \`legacy-kapt\` plugin skips annotation processing unlike \`kotlin-kapt\` | | [Issue #446492061](https://issuetracker.google.com/issues/446492061) compileSdkSpec.minorApiLevel is not working with SettingsExtension | | [Issue #429253579](https://issuetracker.google.com/issues/429253579) \[fused lib - public\] Generated fused library does not include sources | | [Issue #149770867](https://issuetracker.google.com/issues/149770867) extractNativeLibs and useEmbeddedDex should not be coming from the manifest | | [Issue #449114518](https://issuetracker.google.com/issues/449114518) Warnings from R8 in AGP 9.0.0-alpha09 | | [Issue #368426598](https://issuetracker.google.com/issues/368426598) Remove deprecated AndroidSourceSet.jni in AGP 9.0 | | [Issue #368484483](https://issuetracker.google.com/issues/368484483) Remove Installation.installOptions() in AGP 9.0 | | [Issue #368482484](https://issuetracker.google.com/issues/368482484) Remove BuildType.isRenderscriptDebuggable in AGP 9.0. | | [Issue #428646179](https://issuetracker.google.com/issues/428646179) Remove android.defaults.buildfeatures.renderscript | | [Issue #436887358](https://issuetracker.google.com/issues/436887358) \`com.android.kotlin.multiplatform.library\` crashes with Gradle Managed Devices | | [Issue #428645763](https://issuetracker.google.com/issues/428645763) Remove \`android.defaults.buildfeatures.aidl\` defaults gradle.properties flags | | [Issue #294183018](https://issuetracker.google.com/issues/294183018) Fail build when proguard file does not exist | | [Issue #254305041](https://issuetracker.google.com/issues/254305041) remove buildconfig defaults gradle.properties flags | | [Issue #280674230](https://issuetracker.google.com/issues/280674230) Change the app's targetSdk default value to be based on compileSdk instead of minSdk | | [Issue #436878535](https://issuetracker.google.com/issues/436878535) When \`isIncludeAndroidResources\` is enabled, \`process{Variant}UnitTestManifest\` fails to merge tools:overrideLibrary usages in AGP 8.12.0 | | [Issue #411739086](https://issuetracker.google.com/issues/411739086) AGP causing deprecation warnings in Gradle for JVM test tasks | | [Issue #235457021](https://issuetracker.google.com/issues/235457021) DependencyReportTask is incompatible with the configuration cache | | [Issue #369246556](https://issuetracker.google.com/issues/369246556) Switch default source/target Java version from Java 8 to Java 11 in AGP 9.0 | | [Issue #258855275](https://issuetracker.google.com/issues/258855275) Flip android.useAndroidX default to true | | [Issue #442763200](https://issuetracker.google.com/issues/442763200) Better exception when applying kapt plugin with built-in Kotlin. | | [Issue #441679226](https://issuetracker.google.com/issues/441679226) android.proguard.failOnMissingFiles is not working for consumerProguardFiles | | [Issue #443051391](https://issuetracker.google.com/issues/443051391) Update Kotlin Gradle plugin dependency to 2.2.10 | | [Issue #429981132](https://issuetracker.google.com/issues/429981132) Create KotlinJvmAndroidCompilation using KGP API | | [Issue #442869731](https://issuetracker.google.com/issues/442869731) Kotlin explicit API mode applied to test sources | | -| **Lint** | |---| | [Issue #430991549](https://issuetracker.google.com/issues/430991549) AGP 8.11.0: lintAnalyzeRelease task crashes when applying .gradle.kts files with apply(from = "...") | | [Issue #441536820](https://issuetracker.google.com/issues/441536820) Lint ChecksSdkIntAtLeast Check does not check if the annotated value is correct | | [Issue #446696613](https://issuetracker.google.com/issues/446696613) Built-in Kotlin does not add .kotlin_module to META-INF | | [Issue #449031505](https://issuetracker.google.com/issues/449031505) Lint classpath contains duplicate classes at different versions | | [Issue #448148350](https://issuetracker.google.com/issues/448148350) Overriding private resources workaround not working (tools:override = "true") | | [Issue #405676712](https://issuetracker.google.com/issues/405676712) Bug: removal of unused resources doesn't also remove the translations of them, and doesn't ask about it either | | [Issue #440415636](https://issuetracker.google.com/issues/440415636) Lint throwing warning "Could not clean up K2 caches" | | [Issue #440415636](https://issuetracker.google.com/issues/440415636) Lint throwing warning "Could not clean up K2 caches" | | -| **Lint Integration** | |---| | [Issue #460068798](https://issuetracker.google.com/issues/460068798) AndroidLintAnalysisTask cache misses across different JDK vendors or minor versions due to systemPropertyInputs.javaVersion differences | | [Issue #444447002](https://issuetracker.google.com/issues/444447002) Lint automatically uses latest installed SDK despite compileSdk, doesn't register as task input and breaks caching | | -| **Shrinker (R8)** | |---| | [Issue #454927488](https://issuetracker.google.com/issues/454927488) R8 optimized resource shrinking silently fails if using final resource IDs | | +Fixed Issues **Android Gradle Plugin** [Issue #171293712](https://issuetracker.google.com/issues/171293712) Feature Request: Inject ideal AGP version as a property [Issue #443976533](https://issuetracker.google.com/issues/443976533) Stabilize SingleArtifact.VERSION_CONTROL_INFO_FILE [Issue #223643506](https://issuetracker.google.com/issues/223643506) androidTest connectedCheck logcat output is broken [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets [Issue #460094802](https://issuetracker.google.com/issues/460094802) missingDimensionStrategy prefers a flavor maching its own name even from an unrelated dimension [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets [Issue #471410336](https://issuetracker.google.com/issues/471410336) AGP 9.0.0-rc01 doesn't resolve Kotlin libraries via kotlin() function [Issue #450851465](https://issuetracker.google.com/issues/450851465) Built-in-kotlin does not publish kotlin-stdlib dependency constraint in maven pom [Issue #420592288](https://issuetracker.google.com/issues/420592288) Add a test case for divergence between compileSdk and targetSdk [Issue #449153004](https://issuetracker.google.com/issues/449153004) empty resConfigs value leads to obscure aapt error [Issue #447375921](https://issuetracker.google.com/issues/447375921) Pure Java project have dependency on kotlin stdlib. [Issue #368600704](https://issuetracker.google.com/issues/368600704) Remove deprecated KotlinMultiplatformAndroidCompilationBuilder properties in AGP 9.0 [Issue #445209309](https://issuetracker.google.com/issues/445209309) \`com.android.tools.build:gradle:9.0.0-alpha05\` should have an api dependency on KGP and gradle-api [Issue #452645779](https://issuetracker.google.com/issues/452645779) Rename com.android.experimental.built-in-kotlin Gradle plugin [Issue #446220448](https://issuetracker.google.com/issues/446220448) AGP9: \`variant.sources.kotlin!!.addGeneratedSourceDirectory()\` is not working [Issue #448450771](https://issuetracker.google.com/issues/448450771) Aar metadata checks on compile Sdk uses the old DSL [Issue #441523448](https://issuetracker.google.com/issues/441523448) Remove deprecated \`com.android.build.api.dsl.ManagedDevices.devices\` property [Issue #386221070](https://issuetracker.google.com/issues/386221070) Built-in Kotlin support in AGP should not synchronize with the Kotlin sourcesets [Issue #433758231](https://issuetracker.google.com/issues/433758231) Fail android library publishing if consumer keep file contains -dontobfuscate [Issue #241955408](https://issuetracker.google.com/issues/241955408) No Options to Print Mapping for Optimized Resources [Issue #436595826](https://issuetracker.google.com/issues/436595826) Make it an error to call finalizeDsl after this phase has been passed [Issue #457089670](https://issuetracker.google.com/issues/457089670) AGP initializes jetifier config even when jetifier is disabled [Issue #452246814](https://issuetracker.google.com/issues/452246814) built in kotlin does not add kotlinStdlib as a compile time dependency when \`kotlin.stdlib.default.dependency\` is true to module and pom files [Issue #442250902](https://issuetracker.google.com/issues/442250902) New optimizaation DSL does not create configuration.txt by default [Issue #443587266](https://issuetracker.google.com/issues/443587266) AGP 8.13.0 fails to verify nav graph in a module [Issue #444260628](https://issuetracker.google.com/issues/444260628) AGP uses deprecated Gradle API: multi-string notation [Issue #347732357](https://issuetracker.google.com/issues/347732357) Warn users trying to use legacy multidex library with minSdkVersion \>=21 [Issue #333831734](https://issuetracker.google.com/issues/333831734) A build fails if there are code generation tasks [Issue #446123111](https://issuetracker.google.com/issues/446123111) With \`android.builtInKotlin=false\` and \`android.newDsl=false\` and \`android.enableLegacyVariantApi=false\`, using \`kotlin-android\` plugin will fail with "API 'applicationVariants' is obsolete" [Issue #443037365](https://issuetracker.google.com/issues/443037365) Built-in Kotlin fails to resolve unversioned kotlin-stdlib when kotlin.stdlib.default.dependency=false [Issue #445967244](https://issuetracker.google.com/issues/445967244) DexData opens a file without closing, preventing cleanup [Issue #368609737](https://issuetracker.google.com/issues/368609737) AndroidSourceDirectorySet should stop extending PatternFilterable in AGP 9.0 [Issue #389707041](https://issuetracker.google.com/issues/389707041) Test Fixture Error in test only modules [Issue #353249347](https://issuetracker.google.com/issues/353249347) Incorrect error when using context receivers in test fixtures [Issue #351046197](https://issuetracker.google.com/issues/351046197) Incorrect IDE errors for Kotlin code in testFixtures [Issue #446889652](https://issuetracker.google.com/issues/446889652) \`legacy-kapt\` plugin skips annotation processing unlike \`kotlin-kapt\` [Issue #446492061](https://issuetracker.google.com/issues/446492061) compileSdkSpec.minorApiLevel is not working with SettingsExtension [Issue #429253579](https://issuetracker.google.com/issues/429253579) \[fused lib - public\] Generated fused library does not include sources [Issue #149770867](https://issuetracker.google.com/issues/149770867) extractNativeLibs and useEmbeddedDex should not be coming from the manifest [Issue #449114518](https://issuetracker.google.com/issues/449114518) Warnings from R8 in AGP 9.0.0-alpha09 [Issue #368426598](https://issuetracker.google.com/issues/368426598) Remove deprecated AndroidSourceSet.jni in AGP 9.0 [Issue #368484483](https://issuetracker.google.com/issues/368484483) Remove Installation.installOptions() in AGP 9.0 [Issue #368482484](https://issuetracker.google.com/issues/368482484) Remove BuildType.isRenderscriptDebuggable in AGP 9.0. [Issue #428646179](https://issuetracker.google.com/issues/428646179) Remove android.defaults.buildfeatures.renderscript [Issue #436887358](https://issuetracker.google.com/issues/436887358) \`com.android.kotlin.multiplatform.library\` crashes with Gradle Managed Devices [Issue #428645763](https://issuetracker.google.com/issues/428645763) Remove \`android.defaults.buildfeatures.aidl\` defaults gradle.properties flags [Issue #294183018](https://issuetracker.google.com/issues/294183018) Fail build when proguard file does not exist [Issue #254305041](https://issuetracker.google.com/issues/254305041) remove buildconfig defaults gradle.properties flags [Issue #280674230](https://issuetracker.google.com/issues/280674230) Change the app's targetSdk default value to be based on compileSdk instead of minSdk [Issue #436878535](https://issuetracker.google.com/issues/436878535) When \`isIncludeAndroidResources\` is enabled, \`process{Variant}UnitTestManifest\` fails to merge tools:overrideLibrary usages in AGP 8.12.0 [Issue #411739086](https://issuetracker.google.com/issues/411739086) AGP causing deprecation warnings in Gradle for JVM test tasks [Issue #235457021](https://issuetracker.google.com/issues/235457021) DependencyReportTask is incompatible with the configuration cache [Issue #369246556](https://issuetracker.google.com/issues/369246556) Switch default source/target Java version from Java 8 to Java 11 in AGP 9.0 [Issue #258855275](https://issuetracker.google.com/issues/258855275) Flip android.useAndroidX default to true [Issue #442763200](https://issuetracker.google.com/issues/442763200) Better exception when applying kapt plugin with built-in Kotlin. [Issue #441679226](https://issuetracker.google.com/issues/441679226) android.proguard.failOnMissingFiles is not working for consumerProguardFiles [Issue #443051391](https://issuetracker.google.com/issues/443051391) Update Kotlin Gradle plugin dependency to 2.2.10 [Issue #429981132](https://issuetracker.google.com/issues/429981132) Create KotlinJvmAndroidCompilation using KGP API [Issue #442869731](https://issuetracker.google.com/issues/442869731) Kotlin explicit API mode applied to test sources **Lint** [Issue #430991549](https://issuetracker.google.com/issues/430991549) AGP 8.11.0: lintAnalyzeRelease task crashes when applying .gradle.kts files with apply(from = "...") [Issue #441536820](https://issuetracker.google.com/issues/441536820) Lint ChecksSdkIntAtLeast Check does not check if the annotated value is correct [Issue #446696613](https://issuetracker.google.com/issues/446696613) Built-in Kotlin does not add .kotlin_module to META-INF [Issue #449031505](https://issuetracker.google.com/issues/449031505) Lint classpath contains duplicate classes at different versions [Issue #448148350](https://issuetracker.google.com/issues/448148350) Overriding private resources workaround not working (tools:override = "true") [Issue #405676712](https://issuetracker.google.com/issues/405676712) Bug: removal of unused resources doesn't also remove the translations of them, and doesn't ask about it either [Issue #440415636](https://issuetracker.google.com/issues/440415636) Lint throwing warning "Could not clean up K2 caches" [Issue #440415636](https://issuetracker.google.com/issues/440415636) Lint throwing warning "Could not clean up K2 caches" **Lint Integration** [Issue #460068798](https://issuetracker.google.com/issues/460068798) AndroidLintAnalysisTask cache misses across different JDK vendors or minor versions due to systemPropertyInputs.javaVersion differences [Issue #444447002](https://issuetracker.google.com/issues/444447002) Lint automatically uses latest installed SDK despite compileSdk, doesn't register as task input and breaks caching **Shrinker (R8)** [Issue #454927488](https://issuetracker.google.com/issues/454927488) R8 optimized resource shrinking silently fails if using final resource IDs
\ No newline at end of file diff --git a/build-system/agp/agp-9-upgrade/references/buildconfig.md b/build-system/agp/agp-9-upgrade/references/buildconfig.md index e4e19b6..773fcd8 100644 --- a/build-system/agp/agp-9-upgrade/references/buildconfig.md +++ b/build-system/agp/agp-9-upgrade/references/buildconfig.md @@ -1,5 +1,4 @@ -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 @@ -37,11 +36,9 @@ 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", @@ -49,6 +46,4 @@ repository. 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 e8a5d33..2dacb96 100644 --- a/build-system/agp/agp-9-upgrade/references/ksp-kapt.md +++ b/build-system/agp/agp-9-upgrade/references/ksp-kapt.md @@ -1,39 +1,23 @@ -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 a20da2a..97a65bc 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,19 +1,10 @@ -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 d3ecc74..a5d9856 100644 --- a/build-system/agp/agp-9-upgrade/references/recipes.md +++ b/build-system/agp/agp-9-upgrade/references/recipes.md @@ -1,6 +1,4 @@ -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 @@ -8,14 +6,9 @@ migrated. - **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 @@ -56,7 +49,4 @@ 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 6ae6ca1..b9ec31a 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-07-07' + last-updated: '2026-08-06' keywords: - recipe - Android @@ -26,20 +26,17 @@ 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 @@ -62,6 +59,7 @@ run { pending = pending.withAudioEnabled() // Reassignment val active = pending.start(exec, listener) } + ```
@@ -77,9 +75,7 @@ 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. @@ -106,20 +102,16 @@ 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 9c6e80e..03bfee5 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,6 +36,7 @@ LaunchedEffect(context, lifecycleOwner) { ) val cameraControl = camera.cameraControl } + ```
@@ -50,21 +51,25 @@ 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) + ```
@@ -75,6 +80,7 @@ Use `androidx.camera.compose.CameraXViewfinder`. 1. **Set up preview and SurfaceRequest**: +
```kotlin var surfaceRequest by remember { mutableStateOf(null) } @@ -83,12 +89,14 @@ Use `androidx.camera.compose.CameraXViewfinder`. setSurfaceProvider { request -> surfaceRequest = request } } } + ```
2. **Render viewfinder**: +
```kotlin surfaceRequest?.let { request -> @@ -98,12 +106,14 @@ Use `androidx.camera.compose.CameraXViewfinder`. modifier = Modifier ) } + ```
3. **Handle tap-to-focus in Compose**: +
```kotlin // Inside your tap gesture handler... @@ -115,12 +125,14 @@ 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) { @@ -130,15 +142,16 @@ 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( @@ -170,15 +183,16 @@ 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) { @@ -186,6 +200,7 @@ 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 5b21f87..eee1510 100644 --- a/camera/camerax/references/camera2-to-camerax.md +++ b/camera/camerax/references/camera2-to-camerax.md @@ -1,10 +1,6 @@ -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. *** ** * ** *** @@ -21,10 +17,9 @@ 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 @@ -56,6 +51,7 @@ LaunchedEffect(context, lifecycleOwner) { imageAnalysis ) } + ```
@@ -64,17 +60,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) + ```
@@ -83,6 +79,7 @@ preview.setSurfaceProvider(previewView.surfaceProvider) Use `androidx.camera.compose.CameraXViewfinder`. +
```kotlin var surfaceRequest by remember { mutableStateOf(null) } @@ -91,6 +88,7 @@ val preview = remember { setSurfaceProvider { request -> surfaceRequest = request } } } + ```
@@ -99,10 +97,9 @@ 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( @@ -134,6 +131,7 @@ imageCapture.takePicture( } } ) + ```
@@ -142,9 +140,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 -> @@ -157,6 +155,7 @@ imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> imageProxy.close() } } + ```
@@ -165,10 +164,9 @@ 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 @@ -180,6 +178,7 @@ 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 e1f2f25..753347b 100644 --- a/camera/camerax/references/expert-blueprints.md +++ b/camera/camerax/references/expert-blueprints.md @@ -1,6 +1,4 @@ -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 @@ -54,5 +52,4 @@ phases. ## 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 22d378a..f0dd882 100644 --- a/camera/camerax/references/foldables.md +++ b/camera/camerax/references/foldables.md @@ -1,6 +1,4 @@ -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 @@ -16,9 +14,9 @@ postures, such as tabletop and book modes. ### 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 { @@ -33,15 +31,14 @@ 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. @@ -49,9 +46,9 @@ seeing a "bent" image. ### 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) @@ -62,14 +59,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 1dade21..30e11f6 100644 --- a/camera/camerax/references/immutability.md +++ b/camera/camerax/references/immutability.md @@ -1,11 +1,8 @@ -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 | |---|---|---| @@ -20,6 +17,7 @@ reassign the variable. To set up video recording, use the following code: +
```kotlin // WRONG @@ -42,6 +40,7 @@ run { pending = pending.withAudioEnabled() // Reassignment val active = pending.start(exec, listener) } + ```
@@ -50,11 +49,13 @@ 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 e03e00c..3650fa5 100644 --- a/camera/camerax/references/low-light.md +++ b/camera/camerax/references/low-light.md @@ -1,5 +1,4 @@ -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 @@ -13,13 +12,13 @@ extensions** and **Low Light Boost (LLB)**. ## 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 @@ -30,6 +29,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) ) cameraProvider.bindToLifecycle(lifecycleOwner, nightSelector, imageCapture, preview) } + ```
@@ -42,6 +42,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) val imageCapture = ImageCapture.Builder() .setPostviewEnabled(true) .build() + ``` - **Extension strength** : Let users control the intensity of the night effect. @@ -50,6 +51,7 @@ 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. @@ -61,6 +63,7 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) } catch (e: ImageCaptureException) { // Handle capture failure } + ``` *** ** * ** *** @@ -71,8 +74,7 @@ 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** : @@ -80,6 +82,7 @@ auto-exposure algorithm. ```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. @@ -112,6 +115,7 @@ 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 833daf1..0b29bb2 100644 --- a/camera/camerax/references/mlkit-spatial.md +++ b/camera/camerax/references/mlkit-spatial.md @@ -1,6 +1,4 @@ -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 @@ -16,25 +14,23 @@ analysis image and the viewfinder UI. ### 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: @@ -44,20 +40,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 616d549..32f0d56 100644 --- a/camera/camerax/references/modern-apis.md +++ b/camera/camerax/references/modern-apis.md @@ -1,5 +1,4 @@ -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 @@ -38,5 +37,4 @@ 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 972f285..2c24bc3 100644 --- a/camera/camerax/references/testing.md +++ b/camera/camerax/references/testing.md @@ -1,6 +1,4 @@ -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 @@ -17,42 +15,41 @@ patterns for reliable, hermetic camera tests. ### 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 41cfe19..0447f27 100644 --- a/camera/camerax/references/thermals.md +++ b/camera/camerax/references/thermals.md @@ -1,6 +1,4 @@ -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 @@ -16,9 +14,9 @@ force-close the camera app. ### 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 @@ -30,6 +28,7 @@ val preview = Preview.Builder() ) } .build() + ```
@@ -43,9 +42,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 @@ -63,6 +62,7 @@ powerManager.addThermalStatusListener { status -> } } } + ```
diff --git a/camera/camerax/references/wear-os.md b/camera/camerax/references/wear-os.md index 4a942db..11ae45f 100644 --- a/camera/camerax/references/wear-os.md +++ b/camera/camerax/references/wear-os.md @@ -1,6 +1,4 @@ -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 @@ -17,8 +15,7 @@ control the phone's camera. ### 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: @@ -30,6 +27,7 @@ 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 @@ -41,30 +39,31 @@ 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 59fae32..1edfddf 100644 --- a/camera/camerax/references/xr.md +++ b/camera/camerax/references/xr.md @@ -1,5 +1,4 @@ -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 @@ -15,8 +14,7 @@ shift from 2D pixel-pushing to 3D spatial awareness. ### 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. @@ -26,13 +24,13 @@ insufficient. Always use spatial software development kits (SDKs): 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 ad75915..5ad96e6 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-07-28' + last-updated: '2026-08-06' keywords: - AppFunctions - Kotlin @@ -20,29 +20,19 @@ 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: @@ -55,7 +45,7 @@ If users request a subset of steps, you must encourage them to use all steps. If they apply, you must load the following references: -- *[Context and terminology](context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. +- *[Context and terminology](references/context.md)*: Defines the ubiquitous language, architecture definitions, and design patterns across the AppFunctions skill suite. Load when you need to understand core architecture terminology or check the distinctions between modern and legacy AppFunctions APIs. - *[Migration to service entry point](references/migrate-to-service-entry-point.md)* : Documents the systematic procedure for migrating Android applications using AppFunctions versions 1.0.0-alpha09 and lower to `AppFunctionServiceEntryPoint` architecture introduced in version 1.0.0-alpha10. Load when a user asks to migrate or upgrade existing AppFunctions code, or when you encounter legacy `AppFunctionConfiguration.Provider` implementations. ## Critical constraints @@ -67,5 +57,5 @@ If they apply, you must load the following references: - If you encounter build-time errors such as Kotlin Symbol Processing (KSP) issues, see [Implementation and configuration](references/implementation-configuration.md). - If you encounter runtime errors such as missing services or execution failures, see [Testing and debugging](references/adb-interaction-testing.md). -- If you need architecture definitions and vocabulary, see [Context and terminology](context.md). +- If you need architecture definitions and vocabulary, see [Context and terminology](references/context.md). - If you encounter issues when upgrading legacy configurations, see [Migration to service entry point](references/migrate-to-service-entry-point.md). diff --git a/device-ai/appfunctions/context.md b/device-ai/appfunctions/context.md deleted file mode 100644 index dc49d25..0000000 --- a/device-ai/appfunctions/context.md +++ /dev/null @@ -1,26 +0,0 @@ -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. - -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 diff --git a/device-ai/appfunctions/references/adb-interaction-testing.md b/device-ai/appfunctions/references/adb-interaction-testing.md index b487923..b7fac4a 100644 --- a/device-ai/appfunctions/references/adb-interaction-testing.md +++ b/device-ai/appfunctions/references/adb-interaction-testing.md @@ -1,16 +1,13 @@ -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 @@ -19,38 +16,28 @@ 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 @@ -58,8 +45,7 @@ service: app_function` is returned, the device doesn't support this feature. 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 @@ -91,5 +77,4 @@ parameters provided to guide the ADB interaction testing tool interactions. **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 new file mode 100644 index 0000000..9d18e2c --- /dev/null +++ b/device-ai/appfunctions/references/context.md @@ -0,0 +1,11 @@ +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. + +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 diff --git a/device-ai/appfunctions/references/feature-discovery-analysis.md b/device-ai/appfunctions/references/feature-discovery-analysis.md index 583f67c..4adb255 100644 --- a/device-ai/appfunctions/references/feature-discovery-analysis.md +++ b/device-ai/appfunctions/references/feature-discovery-analysis.md @@ -13,15 +13,11 @@ 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 @@ -29,9 +25,7 @@ steps. **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`. @@ -39,33 +33,22 @@ menus. **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 fb9292c..4f8260e 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 ` tag: + ```
@@ -96,16 +98,13 @@ 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) @@ -126,19 +125,16 @@ 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) @@ -163,16 +159,14 @@ 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.
@@ -180,19 +174,17 @@ than creating redundant abstraction layers around the OS service. ### 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. */ @@ -214,6 +206,7 @@ data class Task( /** The content of the task. */ val content: String, ) + ```
@@ -222,6 +215,7 @@ data class Task( ### Example: Implementation detail +
```kotlin @RequiresApi(36) @@ -262,6 +256,7 @@ 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 c0a3209..38ce18e 100644 --- a/device-ai/appfunctions/references/kdoc-refinement-optimization.md +++ b/device-ai/appfunctions/references/kdoc-refinement-optimization.md @@ -5,8 +5,7 @@ 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. @@ -14,8 +13,7 @@ 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." @@ -29,19 +27,15 @@ instructions: ### 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 f350f61..a0528e9 100644 --- a/device-ai/appfunctions/references/migrate-to-service-entry-point.md +++ b/device-ai/appfunctions/references/migrate-to-service-entry-point.md @@ -1,7 +1,4 @@ -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`. *** ** * ** *** @@ -23,9 +20,7 @@ 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`. @@ -38,10 +33,7 @@ strict requirements that you must complete: ### 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 { @@ -66,16 +58,13 @@ within the main `appfunctions` artifact. ### 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) @@ -96,19 +85,16 @@ 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) @@ -133,6 +119,7 @@ abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() { return messageRepository.send(name, endpointValue, messageBody) } } + ```
@@ -144,10 +131,7 @@ 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 @@ -156,8 +140,7 @@ because the wrapper inherently extends `android.content.Context`. ### 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() @@ -166,21 +149,15 @@ Update your `Application` class by removing ### 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 ` tag: + ```
@@ -209,18 +187,7 @@ within the `` tag: ## 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/devtools/android-cli/SKILL.md b/devtools/android-cli/SKILL.md index adb7fad..af4320b 100644 --- a/devtools/android-cli/SKILL.md +++ b/devtools/android-cli/SKILL.md @@ -80,30 +80,45 @@ Update the Android CLI using the `android update` command. # `android help` output -Usage: android [-hV] [--sdk=PARAM] [COMMAND] +Usage: android [-hvV] [--sdk=PARAM] [COMMAND] -h, --help Show this help message and exit. --sdk=PARAM Path to the Android SDK + -v, --verbose Enable verbose output for troubleshooting -V, --version Print version information and exit. Commands: - create Create a new Android project + create Create a new Android project from available templates. Allows + specifying project name, output directory, minSdk, and dry-run + execution. describe Analyzes an Android project to generate descriptive metadata. - docs Android documentation commands - emulator Emulator commands + docs Android documentation commands for searching and fetching developer + documentation from the official knowledge base. + emulator Manage Android Virtual Devices (AVDs). Includes commands to start, + stop, list, and view details about emulators. help Shows the help of all commands - info Print environment information (SDK Location, etc.) - init Initializes the environment (eg. skills) for Android CLI. + info Print environment information including SDK location, connected + devices, and configuration variables. Use specific fields to + narrow the output. + init Initializes the environment for the Android CLI. Sets up required + configurations, directories, and default skills. + install Install an Android Application (one or more APKs) to a connected + device or emulator without activating any components using + incremental optimizations for faster deployment than adb. layout Returns the layout tree of an application - run Deploy an Android Application + run Build, deploy, and launch an Android application on a connected + device or emulator. screen Commands to view the device - sdk Download and list SDK packages - skills Manage skills + sdk Manage the Android SDK installation. Includes commands to install, + update, remove, and list available and installed SDK packages. + skills Manage Android CLI skills. Includes commands to install, remove, + list, and search for skills by keyword. studio Android Studio commands - update Update the Android CLI + update Update the Android CLI to the latest version. create Usage: android create [-h] [--verbose] [--list] [--minSdk=api] --name=applicationName [-o=dest-path] [template-name] - Create a new Android project + Create a new Android project from available templates. Allows specifying + project name, output directory, minSdk, and dry-run execution. [template-name] The template name -h, --help Show this help message and exit. --minSdk=api The 'minSdk' supported by the application (default @@ -128,15 +143,17 @@ describe docs Usage: android docs [-h] [COMMAND] - Android documentation commands + Android documentation commands for searching and fetching developer + documentation from the official knowledge base. -h, --help Show this help message and exit. Commands: - search Search Android documentation - fetch Fetch Android documentation + search Search Android documentation. Enclose keywords in quotes. + fetch Fetch an Android documentation article from a URL (kb://...) emulator Usage: android emulator [-h] [COMMAND] - Emulator commands + Manage Android Virtual Devices (AVDs). Includes commands to start, stop, list, + and view details about emulators. -h, --help Show this help message and exit. Commands: create Creates a virtual device @@ -153,12 +170,33 @@ help info Usage: android info - Print environment information (SDK Location, etc.) + Print environment information including SDK location, connected devices, and + configuration variables. Use specific fields to narrow the output. The specific field to print the value of. If omitted print all. init Usage: android init - Initializes the environment (eg. skills) for Android CLI. + Initializes the environment for the Android CLI. Sets up required + configurations, directories, and default skills. + +install + Usage: android install [-hv] [--use-delta-install] [--device=PARAM] + [--apks=PARAM[,PARAM...]]... [--install-options=PARAM[, + PARAM...]]... + Install an Android Application (one or more APKs) to a connected device or + emulator without activating any components using incremental optimizations for + faster deployment than adb. + --apks=PARAM[,PARAM...] + The paths to the APKs + --device=PARAM The device serial number + -h, --help Show this help message and exit. + --install-options=PARAM[,PARAM...] + Additional options/flags to pass to package manager + install (e.g. -g,-d) + --use-delta-install Use fast delta install (speeds up incremental + updates by transferring only modified code and + resources; default: true) + -v, --verbose Enable verbose logging layout Usage: android layout [-dhp] [--device=PARAM] [-o=PARAM] @@ -173,16 +211,26 @@ layout -p, --pretty Pretty-prints the returned JSON run - Usage: android run [-h] [--debug] [--activity=PARAM] [--device=PARAM] - [--type=PARAM] [--apks=PARAM[,PARAM...]]... - Deploy an Android Application - --activity=PARAM The activity name + Usage: android run [-hv] [--debug] [--use-delta-install] [--activity=PARAM] + [--device=PARAM] [--type=PARAM] [--apks=PARAM[,PARAM...]]... + [--install-options=PARAM[,PARAM...]]... + Build, deploy, and launch an Android application on a connected device or + emulator. + --activity=PARAM The activity name --apks=PARAM[,PARAM...] - The paths to the APKs - --debug Run in debug mode - --device=PARAM The device serial number - -h, --help Show this help message and exit. - --type=PARAM The component type (ACTIVITY, SERVICE, etc.) + The paths to the APKs + --debug Run in debug mode + --device=PARAM The device serial number + -h, --help Show this help message and exit. + --install-options=PARAM[,PARAM...] + Additional options/flags to pass to package manager + install (e.g. -g,-d) + --type=PARAM The component type (ACTIVITY, WATCH_FACE, TILE, + COMPLICATION, DECLARATIVE_WATCH_FACE, WEAR_WIDGET) + --use-delta-install Use fast delta install (speeds up incremental + updates by transferring only modified code and + resources; default: false) + -v, --verbose Enable verbose logging screen Usage: android screen [-h] [COMMAND] @@ -194,21 +242,25 @@ screen sdk Usage: android sdk [COMMAND] - Download and list SDK packages + Manage the Android SDK installation. Includes commands to install, update, + remove, and list available and installed SDK packages. Commands: install Install SDK packages update Update one or all packages to the latest version - remove Remove a package from the SDK + remove Remove packages from the SDK list List installed and available SDK packages skills - Usage: android skills [COMMAND] - Manage skills + Usage: android skills [-h] [COMMAND] + Manage Android CLI skills. Includes commands to install, remove, list, and + search for skills by keyword. + -h, --help Show this help message and exit. Commands: - add Install a skill - remove Remove a skill - list List available skills - find Find skills by keyword + add [id] Install a specific skill by its ID to your environment + remove [id] Remove an installed skill by its ID + list List installed and available skills + find [keyword] Search for available skills in the repository matching a + keyword studio Usage: android studio [-h] [COMMAND] @@ -227,5 +279,6 @@ studio update Usage: android update [--url=PARAM] - Update the Android CLI + Update the Android CLI to the latest version. --url=PARAM The URL to download the update from + diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 784038e..0b8b191 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-07-02' + last-updated: '2026-08-06' keywords: - implementation - Android @@ -57,15 +57,11 @@ 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"` @@ -76,14 +72,11 @@ screens with different use cases to implement verified email: ## 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: @@ -92,16 +85,14 @@ 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` @@ -117,21 +108,20 @@ actions: - Leave a TODO for developers to handle the app's server-side validation and parsing. - Direct users to the home screen after API call success and show a snackbar with user details for reference purpose only. -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. +## 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. ## 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 ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -139,8 +129,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` @@ -154,14 +144,9 @@ 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() @@ -206,26 +191,19 @@ providers support a JSON structure with an outer `"digital": {"requests": 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). @@ -261,15 +239,12 @@ 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) @@ -294,12 +269,9 @@ The following code extracts the raw [Selective Disclosure JWT ## 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: @@ -337,18 +309,11 @@ additional metadata as well along with verified email: ## 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. @@ -359,15 +324,11 @@ 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. @@ -390,17 +351,11 @@ intercepted or spoofed before provisioning the new account. ## 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 @@ -409,8 +364,7 @@ actual call to the Credential Manager API. ## 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 c646f19..d11d29b 100644 --- a/identity/verified-email/references/android/identity/credential-manager/index.md +++ b/identity/verified-email/references/android/identity/credential-manager/index.md @@ -1,21 +1,15 @@ -[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). @@ -23,12 +17,10 @@ offering a robust set of capabilities, including: ## 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: @@ -36,8 +28,7 @@ application and your users: ## 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. @@ -45,21 +36,13 @@ party**. An authentication workflow typically includes the following components: ## 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 3f7f9c9..082ada7 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,14 +1,6 @@ -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 @@ -37,8 +29,7 @@ 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. @@ -51,8 +42,7 @@ Construct a digital credential request and use it to initialize a 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": [ @@ -100,14 +90,9 @@ this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.ht #### 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 9305e92..048f11b 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,22 +1,31 @@ -> [!IMPORTANT] -> **Important:** We released an agent skill to help you implement email verification with the Digital Credentials API. Try out the skill from the [Android skills -> repository](https://github.com/android/skills). +
-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. +## 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: + + android skills add --skill verified-email + +
+ +## 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. ## 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 ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -24,8 +33,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` @@ -39,14 +48,9 @@ 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() @@ -91,26 +95,19 @@ providers support a JSON structure with an outer `"digital": {"requests": 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). @@ -146,15 +143,12 @@ 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) @@ -179,12 +173,9 @@ The following code extracts the raw [Selective Disclosure JWT ## 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: @@ -222,18 +213,11 @@ additional metadata as well along with verified email: ## 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. @@ -244,15 +228,11 @@ 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. @@ -275,17 +255,11 @@ intercepted or spoofed before provisioning the new account. ## 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 2134577..66a5f1a 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,7 +1,4 @@ -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: @@ -19,90 +16,66 @@ 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. @@ -110,30 +83,19 @@ Credentials](https://developer.android.com/identity/digital-credentials#verified ## 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 5f9ca67..0663d39 100644 --- a/identity/verified-email/references/android/identity/digital-credentials/index.md +++ b/identity/verified-email/references/android/identity/digital-credentials/index.md @@ -1,32 +1,20 @@ -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. @@ -37,52 +25,32 @@ 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 bfe19e5..3a190a3 100644 --- a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -1,11 +1,6 @@ -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). @@ -14,15 +9,11 @@ app server. ## 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. @@ -33,8 +24,7 @@ server](https://developer.android.com/identity/credential-manager#authentication 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 @@ -44,8 +34,8 @@ Add the following dependencies to your app module's `build.gradle` file: ```kotlin dependencies { - implementation("androidx.credentials:credentials:1.7.0-alpha02") - implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") } ``` @@ -53,8 +43,8 @@ dependencies { ```groovy dependencies { - implementation "androidx.credentials:credentials:1.7.0-alpha02" - implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" } ``` @@ -71,26 +61,16 @@ 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": "", @@ -149,9 +129,7 @@ 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: @@ -160,25 +138,14 @@ 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] @@ -186,11 +153,9 @@ sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/mai ## 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: @@ -241,79 +206,55 @@ 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) @@ -64,25 +61,21 @@ 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: @@ -94,10 +87,7 @@ 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: @@ -107,6 +97,7 @@ Steps to migrate: For example: +
```kotlin // Pass this variable to any composable that needs to control the navigation area visibility @@ -127,33 +118,26 @@ 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 @@ -163,8 +147,7 @@ presentation). #### 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()`. @@ -173,14 +156,11 @@ presentation). - 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` @@ -195,15 +175,13 @@ screen complements the main screen and is shown in a supporting pane. ### 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: @@ -214,24 +192,16 @@ 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( @@ -250,26 +220,23 @@ 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 @@ -286,14 +253,11 @@ 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 41112fc..27c2e3d 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,17 +14,16 @@ 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`. @@ -33,13 +32,11 @@ items are laid out in. It accepts the following values: ## 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) | @@ -52,9 +49,7 @@ table shows the behavior when the direction is `Row`. ### 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`: @@ -62,10 +57,7 @@ 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) | @@ -73,9 +65,7 @@ is `Row`: ## 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`** | @@ -83,29 +73,20 @@ using `wrap`. | `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 8b00b01..2bf41f0 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,11 +2,10 @@ 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-beta02" + compose = "1.12.0-rc01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } @@ -19,9 +18,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( @@ -33,16 +32,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( @@ -59,11 +58,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 ddda0de..50828f0 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,10 +1,7 @@ > [!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: @@ -14,23 +11,16 @@ 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`. @@ -49,33 +39,18 @@ 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 e4a5c0f..ddb0f02 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,48 +31,31 @@ distributed. You can think of this as the item's *preferred* size. | 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 { @@ -91,6 +74,7 @@ FlexBox { .flex { shrink(0f) } ) } + ```
@@ -105,14 +89,11 @@ 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( @@ -126,6 +107,7 @@ FlexBox( PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) }) OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) }) } + ```
@@ -134,19 +116,13 @@ 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 { @@ -163,6 +139,7 @@ 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 33b37a2..28c8d13 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,6 +1,4 @@ -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. @@ -9,13 +7,9 @@ This page describes how to do the following: ## 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( @@ -29,26 +23,18 @@ 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( @@ -68,15 +54,16 @@ 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( @@ -98,6 +85,7 @@ Grid( Card5() Card6() } + ```
@@ -106,17 +94,16 @@ 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). -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( @@ -136,6 +123,7 @@ Grid( PastelYellowCard("Auto") } + ```
@@ -144,14 +132,11 @@ 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( @@ -171,6 +156,7 @@ Grid( PastelGreenCard("Flex(1.fr)") PastelBlueCard("Fixed(200.dp)") } + ```
@@ -179,18 +165,11 @@ 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( @@ -205,7 +184,7 @@ Grid( }, modifier = Modifier.size(width = 170.dp, height = 240.dp) ) { - Text("Lazy column in a Grid") + Text("LazyColumn in a Grid") // The LazyColumn is placed in the second row, filling the remaining space. LazyColumn(verticalArrangement = Arrangement.spacedBy(4.dp)) { items(100) { number -> @@ -213,6 +192,7 @@ Grid( } } } + ```
@@ -221,20 +201,15 @@ 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( @@ -248,6 +223,7 @@ Grid( Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") } + ```
@@ -256,13 +232,9 @@ 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( @@ -284,17 +256,16 @@ 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( @@ -315,6 +286,7 @@ Grid( Card5() Card6() } + ```
\ 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 bb78a97..0a525c7 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,11 +2,10 @@ 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-beta02" + compose = "1.12.0-rc01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } @@ -19,9 +18,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( @@ -41,11 +40,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 316b046..e48d9c9 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,27 +1,16 @@ > [!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 | |---|---| @@ -34,24 +23,16 @@ of which layouts are suitable for each API: ## 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 @@ -61,13 +42,10 @@ 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 9f0500e..5c8787c 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,33 +1,18 @@ -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. -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( @@ -45,6 +30,7 @@ Grid( Card2(modifier = Modifier.gridItem(row = 2, column = 2)) Card3(modifier = Modifier.gridItem(row = -1, column = -2)) } + ```
@@ -55,16 +41,9 @@ 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( @@ -84,6 +63,7 @@ Grid( Card3() Card4(modifier = Modifier.gridItem(columnSpan = 3)) } + ```
@@ -92,11 +72,9 @@ 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( @@ -124,6 +102,7 @@ Grid( Card3() Card4(modifier = Modifier.gridItem(columnSpan = 3)) } + ```
@@ -133,13 +112,9 @@ 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( @@ -161,6 +136,7 @@ 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 4cd2564..f9fb3ed 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,10 +1,7 @@ > [!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 @@ -12,13 +9,9 @@ In addition to that, you can refer to the following information: - 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. The following example switches the layout to `TabletopLayout` when the foldable posture is tabletop: +
```kotlin @Composable @@ -33,16 +26,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() { @@ -51,20 +44,14 @@ 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 | |---|---|---| @@ -77,17 +64,9 @@ Table 1 describes the parameters available in `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 @@ -102,26 +81,18 @@ 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 { @@ -135,6 +106,7 @@ when { narrowerThanExpanded -> TwoPaneLayout() else -> ThreePaneLayout() } + ```
@@ -143,12 +115,9 @@ 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 { @@ -156,32 +125,22 @@ 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 }) { @@ -189,38 +148,31 @@ 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 { @@ -234,25 +186,18 @@ 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 { @@ -260,19 +205,16 @@ 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 { @@ -280,14 +222,12 @@ 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. @@ -296,6 +236,7 @@ You can override it with the following steps: You can preview the `TabletopLayout` with the following example: +
```kotlin @Preview @@ -324,6 +265,7 @@ 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 023836f..027181b 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,60 +2,29 @@ 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 @@ -68,47 +37,24 @@ 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 fbaae79..032a997 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,4 +138,6 @@ 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 ea25f8e..c51cc9d 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-07-02' + last-updated: '2026-08-06' keywords: - Jetpack Compose - migration @@ -20,16 +20,11 @@ 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 @@ -48,61 +43,31 @@ integrity. ### 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 @@ -113,12 +78,8 @@ 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 3e2f0c3..8b34a75 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,41 +1,22 @@ ## 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 60b7968..3eb0b20 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,14 +1,8 @@ -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 @@ -29,26 +23,19 @@ 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: @@ -69,8 +56,7 @@ 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. @@ -117,20 +103,14 @@ 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:** @@ -162,10 +142,6 @@ components. ## 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 83c7188..a0bc6ad 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,10 +3,9 @@ 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() { @@ -25,6 +24,7 @@ class ExampleActivity : ComponentActivity() { fun Greeting(name: String) { Text(text = "Hello $name!") } + ```
@@ -32,30 +32,17 @@ 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 | |---|---| @@ -69,11 +56,7 @@ The table below summarizes the different scenarios you can use > [!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`: @@ -93,13 +76,12 @@ 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() { @@ -125,13 +107,14 @@ 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() { @@ -167,20 +150,18 @@ 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() { @@ -203,15 +184,16 @@ 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() { @@ -242,6 +224,7 @@ class ExampleFragmentMultipleComposeView : Fragment() { ) } } + ```
@@ -253,18 +236,16 @@ 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 @@ -272,13 +253,12 @@ Layout Editor. 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 14b9121..248d909 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,22 +1,13 @@ -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 @@ -54,6 +45,8 @@ fun ContentExample() { CustomView() } } + + ```
@@ -63,14 +56,12 @@ 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 @@ -79,23 +70,21 @@ 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 @@ -117,72 +106,50 @@ 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 @@ -194,18 +161,16 @@ 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 @@ -247,18 +212,16 @@ 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() { @@ -281,6 +244,7 @@ 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 bf06fa0..ceda68a 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,9 +1,7 @@ -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 @@ -21,8 +19,7 @@ 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 @@ -36,14 +33,11 @@ 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 @@ -59,9 +53,7 @@ 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 { ... } @@ -74,10 +66,9 @@ 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.00`. +Always use the latest Compose BOM version: `2026.06.01`. -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: @@ -103,7 +94,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - def composeBom = platform('androidx.compose:compose-bom:2026.06.00') + def composeBom = platform('androidx.compose:compose-bom:2026.06.01') implementation composeBom androidTestImplementation composeBom @@ -142,7 +133,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - val composeBom = platform("androidx.compose:compose-bom:2026.06.00") + val composeBom = platform("androidx.compose:compose-bom:2026.06.01") implementation(composeBom) androidTestImplementation(composeBom) @@ -178,20 +169,12 @@ 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 650ac92..e8a42d0 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,18 +14,12 @@ 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 5aeb6b3..ba27ac4 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,7 +1,6 @@ ## 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`). @@ -9,9 +8,7 @@ You must determine: ## 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 @@ -23,8 +20,7 @@ nature. ## 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. @@ -44,6 +40,7 @@ Example before migration: Example after migration: +
```kotlin @Composable @@ -73,6 +70,7 @@ fun RoundedBorderlessButton( ) } } + ```
diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index c223382..eb97976 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-07-07' + last-updated: '2026-08-06' keywords: - Jetpack Compose - Styles @@ -30,13 +30,11 @@ 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 { @@ -49,12 +47,9 @@ block to your module's `build.gradle.kts`: 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 @@ -71,6 +66,7 @@ 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 { @@ -81,13 +77,14 @@ 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 @@ -104,13 +101,14 @@ 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 @@ -121,14 +119,15 @@ 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. @@ -137,8 +136,7 @@ sequence: - **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`. @@ -148,6 +146,7 @@ sequence: Before Migration: +
```kotlin @Composable @@ -172,12 +171,14 @@ fun CustomButton( content = content, ) } + ```
After Migration: +
```kotlin // Exposed via ComponentStyles.kt @@ -215,6 +216,7 @@ 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 59dbb25..ae05b5a 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,7 +1,4 @@ -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: @@ -9,21 +6,15 @@ 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 @@ -38,26 +29,21 @@ 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 @@ -96,22 +82,20 @@ 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 @@ -131,13 +115,14 @@ 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 @@ -149,19 +134,18 @@ 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 @@ -222,22 +206,21 @@ 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 @@ -259,13 +242,14 @@ 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 @@ -277,33 +261,28 @@ 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 @@ -383,23 +362,20 @@ 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 @@ -438,6 +414,7 @@ fun CustomButton( } val ButtonShape = RoundedCornerShape(percent = 50) + ```
@@ -445,11 +422,7 @@ 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 ab0d26d..901315d 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,10 +6,7 @@ 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 | |---|---|---| @@ -36,6 +33,7 @@ of properties. Components that expose a `Style` parameter allow you to set their styling: +
```kotlin BaseButton( @@ -44,13 +42,14 @@ 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( @@ -59,19 +58,18 @@ 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( @@ -79,13 +77,14 @@ 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( @@ -95,6 +94,7 @@ Row( ) { BaseText("Content") } + ```
@@ -102,35 +102,28 @@ 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) } @@ -147,12 +140,14 @@ Column( ) { BaseText("Column content") } + ```
You can also pass that Style into multiple components: +
```kotlin val style = Style { background(Color.Blue) } @@ -176,15 +171,16 @@ 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( @@ -196,6 +192,7 @@ BaseButton( ) { BaseText("Button") } + ```
@@ -203,12 +200,9 @@ 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( @@ -227,6 +221,7 @@ BaseButton( ) { BaseText("Click me!") } + ```
@@ -236,9 +231,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) } @@ -252,6 +247,7 @@ BaseButton( ) { BaseText("Click me!") } + ```
@@ -259,12 +255,9 @@ 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 { @@ -285,12 +278,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. @@ -299,9 +292,7 @@ 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. @@ -317,9 +308,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) } @@ -336,6 +327,7 @@ Column( BaseText("certain properties") BaseText("from their parents") } + ```
@@ -345,10 +337,9 @@ 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) } @@ -367,6 +358,7 @@ Column( BaseText("override properties") BaseText("set by their parents") } + ```
@@ -376,39 +368,40 @@ 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 { @@ -416,6 +409,7 @@ 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 01683bd..8c387a6 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,15 +1,8 @@
-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 @@ -21,15 +14,13 @@ 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 @@ -56,16 +47,17 @@ 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 @@ -99,6 +91,7 @@ private fun OpenButton_CombinedStates() { } ) } + ```
@@ -107,18 +100,15 @@ 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 @@ -147,13 +137,14 @@ 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 @@ -174,6 +165,7 @@ fun LoginButton() { BaseText("Login") } } + ```
@@ -182,12 +174,9 @@ 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 { @@ -222,16 +211,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 { @@ -267,6 +256,7 @@ fun AnimatingStyleChangesSpec() { ) .styleable(styleState, animatingStyleSpec)) } + ```
@@ -275,11 +265,7 @@ 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 @@ -287,11 +273,9 @@ use your own custom state: ### 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 { @@ -301,16 +285,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 @@ -324,16 +308,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 @@ -352,13 +336,14 @@ 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 @@ -385,12 +370,14 @@ fun StyleStateKeySample() { style = style, state = PlayerState.Stopped) } + ```
The following code is the full snippet for this example: +
```kotlin enum class PlayerState { @@ -456,6 +443,7 @@ 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 cd9bc50..e6a756b 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,7 +1,4 @@ -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. @@ -19,8 +16,7 @@ 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. @@ -29,18 +25,15 @@ address some limitations of modifiers, which the following list describes: ## 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 c8a373b..e80dfb9 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,8 +1,7 @@ > [!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. @@ -11,15 +10,9 @@ depends on where your app sits in relation to its adoption of Material Design: ## 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 | |---|---|---| @@ -32,11 +25,9 @@ subsystems and components: **Styles**. ### 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 @@ -69,19 +60,18 @@ 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 { @@ -89,49 +79,39 @@ 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. - -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`: +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`: ```kotlin object JetsnackStyles{ @@ -154,13 +134,12 @@ access your base styles from anywhere in your project. 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 @@ -215,13 +194,13 @@ access your base styles from anywhere in your project. ) } } + ```
3. Access `JetsnackStyles` within your composable: - ```kotlin @Composable fun CustomButton(modifier: Modifier, @@ -245,13 +224,9 @@ access your base styles from anywhere in your project. 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 new file mode 100644 index 0000000..e6fb809 --- /dev/null +++ b/media/media3-cast-integration/SKILL.md @@ -0,0 +1,224 @@ +--- +name: media3-cast-integration +description: Implements Google Cast support in Android apps using Jetpack Media3. + Handles adding build dependencies, updating manifest, configuring OptionsProvider, + and managing CastPlayer or RemoteCastPlayer for playback in both Compose and View-based + UIs. Use when adding Cast functionality or migrating from legacy Cast SDK to Media3 + Cast. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-08-06' + keywords: + - Android + - Media3 + - Cast + - Integration + - Migration + - ExoPlayer + - MediaSession + - Jetpack Media3 +--- + +## Prerequisites + +- Jetpack Media3 version must be `>= 1.9.0`. Cast isn't available in lower versions. + +## Glossary + +- **`CastPlayer`** : Media3 `Player` that controls playback on both local and remote Cast devices. +- **`RemoteCastPlayer`** : Media3 `Player` that communicates with a Cast receiver, only used for remote playback. +- **Google Cast SDK**: Legacy casting SDK in maintenance mode, superseded by Jetpack Media3. +- **`OptionsProvider`** : Interface providing configuration options to initialize GMS `CastContext`. + +## Common guidelines + +- Legacy [Google Cast SDK](https://developers.google.com/cast) is in maintenance mode. +- For new Cast setups: + - You must use [Jetpack Media3 Cast](references/android/media/media3/cast/index.md). + - You mustn't use legacy Cast SDK unless explicitly requested. + +## Step 1: Set up dependencies + +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. + + implementation("androidx.media3:media3-cast:1.10.1") + +- Ensure required Media3 dependencies are present: + + - `androidx.media3:media3-exoplayer` + - `androidx.media3:media3-session` + - `androidx.media3:media3-ui-compose` +- If the application uses legacy Views, add `media3-ui`. + +- 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. + +- **For apps without an existing Cast integration:** + + - Verify legacy Cast SDK (`libs.play.services.cast.framework`) is absent. +- **If Migrating from Legacy Cast SDK:** + + - Add Media3 Cast dependencies first. + - Keep existing legacy dependencies untouched at this stage to prevent compilation errors. + +## Step 2: Update the manifest + +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). +- 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:** + - Don't delete existing custom options provider files or manifest entries. + +## Step 3: Implement the player and service + +### Architecture baseline + +Before integrating Media3 Cast, an existing app follows one of two setups: + +- **Local-only playback:** Uses Media3 `ExoPlayer` only to support local playback. +- **Legacy Cast setup:** Uses `ExoPlayer` for local playback, alongside a `Player` wrapper over the legacy `RemoteMediaClient` for remote playback. The UI interfaces with a `MediaSession` interacting with a `ForwardingPlayer`, which finally routes controls to either local or remote playback. + +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). +- 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. + +### Advanced: `RemoteCastPlayer` + +- Use `RemoteCastPlayer` only if explicitly requested by user. +- Initialize `MediaSession` with `localPlayer` and set a `SessionAvailabilityListener` on `RemoteCastPlayer` to transfer playback state on Cast session availability changes: + + class PlaybackService : MediaSessionService() { + private var mediaSession: MediaSession? = null + private lateinit var localPlayer: ExoPlayer + private lateinit var remotePlayer: RemoteCastPlayer + + override fun onCreate() { + super.onCreate() + + localPlayer = ExoPlayer.Builder(this).build() + remotePlayer = RemoteCastPlayer.Builder(this).build() + mediaSession = MediaSession.Builder(this, localPlayer).build() + + remotePlayer.setSessionAvailabilityListener( + object : SessionAvailabilityListener { + override fun onCastSessionAvailable() { + transferPlaybackState(localPlayer, remotePlayer) + } + + override fun onCastSessionUnavailable() { + transferPlaybackState(remotePlayer, localPlayer) + } + } + ) + } + + private fun transferPlaybackState(previousPlayer: Player, newPlayer: Player) { + if (previousPlayer.mediaItemCount > 0) { + val transferStateBuilder = PlayerTransferState.builderFromPlayer(previousPlayer) + if (previousPlayer.playbackState == Player.STATE_ENDED || + previousPlayer.currentPosition == C.TIME_END_OF_SOURCE) { + transferStateBuilder.setCurrentMediaItemIndex(0) + transferStateBuilder.setCurrentPosition(0) + } + transferStateBuilder.build().setToPlayer(newPlayer) + } + + previousPlayer.stop() + previousPlayer.clearMediaItems() + newPlayer.prepare() + mediaSession?.setPlayer(newPlayer) + } + } + +## Step 4: Set up the UI + +### Compose-based UI + +To complete this step, you **MUST** ensure the following: + +- See the "Add a MediaRouteButton Composable to the Player" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md) for Compose integration guidelines. +- 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: + + @OptIn(UnstableApi::class) + @Composable + fun MainScreen() { + val player = rememberMediaController() + val deviceInfo = rememberDeviceInfo(player) + player?.let { activePlayer -> key(deviceInfo) { PlayerScreen(player = activePlayer) } } + } + + @Composable + private fun rememberMediaController(): Player? { + // Logic to connect MediaController to MediaSession and release it + } + + @Composable + private fun rememberDeviceInfo(player: Player?): DeviceInfo? { + var deviceInfo by remember(player) { mutableStateOf(player?.deviceInfo) } + DisposableEffect(player) { + val activePlayer = player ?: return@DisposableEffect onDispose {} + deviceInfo = activePlayer.deviceInfo + val listener = object : Player.Listener { + override fun onDeviceInfoChanged(info: DeviceInfo) { + deviceInfo = info + } + } + activePlayer.addListener(listener) + onDispose { activePlayer.removeListener(listener) } + } + return deviceInfo + } + +### View-based UI + +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). +- 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: + + private val playerListener: Player.Listener = + object : Player.Listener { + override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) { + // Resetting to null bypasses PlayerView.setPlayer()'s instance equality check + // (this.player == player), forcing it to re-bind the video surface to the controller. + playerView.player = null + playerView.player = controller + } + } + +- **Migration to Compose:** + + - Don't use `AndroidView` to wrap the legacy `PlayerView`. + - Implement Material3 [`Player` composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable) and [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) as per [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md). + - Remove legacy XML layout declarations, menu files, and View component references. + +## Step 5: Clean up legacy Cast SDK code + +> [!WARNING] +> **Warning:** Don't perform cleanup directly. Remove legacy files and dependencies only when explicitly requested by the user. + +To complete this step, you **MUST** ensure the following: + +- Remove legacy GMS Cast SDK (`libs.play.services.cast.framework`) and MediaRouter (`libs.androidx.mediarouter`) dependencies. +- Delete custom `OptionsProvider` classes and manifest entries if `DefaultCastOptionsProvider` is adopted. +- Remove legacy `MediaTransferReceiver` manifest declarations if present. +- Remove all references to legacy Cast SDK components such as legacy helper wrappers, forwarding players, and `RemoteMediaClient` interfaces. +- Delete legacy View XML layouts, menu files, and references to `PlayerView` if the migration to Compose is complete. 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 new file mode 100644 index 0000000..9edd7bb --- /dev/null +++ b/media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md @@ -0,0 +1,367 @@ +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. + +## Add build dependencies + +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") + +### 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" + +## 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. + +### 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. + + + ... + + ... + + +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). + + + ... + + ... + + +## 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. +![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 + +```kotlin +override fun onCreate() { + super.onCreate() + + val exoPlayer = ExoPlayer.Builder(context).build() + val castPlayer = CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build() + + mediaSession = MediaSession.Builder(context, castPlayer).build() +} + + +``` + +### Java + +```java +@Override +public void onCreate() { + super.onCreate(); + + ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build(); + CastPlayer castPlayer = new CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build(); + + mediaSession = + new MediaSession.Builder(/* context= */ context, /* player= */ castPlayer).build(); +} + + +``` + +
+ +> [!IMPORTANT] +> **Important:** The preceding code snippet shows the `onCreate` method of a `MediaSessionService`. In an `Activity`, players should be created in `onStart` or `onResume`. + +## 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. + +### 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. + +- **Compose UI**: Add a button composable. +- **Views UI** : + - Add the button to the app bar menu. + - Add the button inside `PlayerView`. + - Add the button as a standard `View`. + +![Screenshot showing the MediaRouteButton in the UI.](https://developer.android.com/static/media/media3/cast/images/cast.jpeg) Figure 2: (a) MediaRouteButton in menu bar, (b) as a View, (c) in PlayerView, and (d) Dialog of Cast-enabled devices. + +> [!IMPORTANT] +> **Important:** To use the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in Views UI, the containing activity must be a subclass of `FragmentActivity`. + +> [!TIP] +> **Tip:** These screenshots are from the [demo-session](https://github.com/androidx/media/tree/release/demos/session) apps in Media3. You can checkout the app to see an example of `CastPlayer` implementation. + +#### 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. + +
+ +```kotlin +@Composable +fun PlayerComposeView(player: Player, modifier: Modifier = Modifier) { + var controlsVisible by remember { mutableStateOf(false) } + + Box( + modifier = modifier.clickable { controlsVisible = true }, + contentAlignment = Alignment.Center, + ) { + PlayerSurface(player = player, modifier = modifier) + AnimatedVisibility(visible = controlsVisible, enter = fadeIn(), exit = fadeOut()) { + Box(modifier = Modifier.fillMaxSize()) { + MediaRouteButton(modifier = Modifier.align(Alignment.TopEnd)) + PrimaryControls(player = player, modifier = Modifier.align(Alignment.Center)) + } + } + } +} + +@Composable +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. + +
+ +### Kotlin + +```kotlin +override fun onStart() { + super.onStart() + + playerView.player = mediaController + playerView.setMediaRouteButtonViewProvider(MediaRouteButtonViewProvider()) +} + +``` + +### Java + +```java +@Override +public void onStart() { + super.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`. + + + + + +
+ +### Kotlin + +```kotlin +override fun onCreateOptionsMenu(menu: Menu): Boolean { + // ... + menuInflater.inflate(R.menu.sample_media_route_button_menu, menu) + val menuItemFuture: ListenableFuture = + MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item) + Futures.addCallback( + menuItemFuture, + object : FutureCallback { + override fun onSuccess(menuItem: MenuItem?) { + // Do something with the menu item. + } + + override fun onFailure(t: Throwable) { + // Handle the failure. + } + }, + executor, + ) + // ... + return true +} + +``` + +### Java + +```java +@Override +public boolean onCreateOptionsMenu(Menu menu) { + // ... + getMenuInflater().inflate(R.menu.sample_media_route_button_menu, menu); + ListenableFuture menuItemFuture = + MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item); + Futures.addCallback( + menuItemFuture, + new FutureCallback() { + @Override + public void onSuccess(MenuItem menuItem) { + // Do something with the menu item. + } + + @Override + public void onFailure(Throwable t) { + // Handle the failure. + } + }, + executor); + // ... + return true; +} + +``` + +
+ +#### Add the `MediaRouteButton` as a View + +You can set up a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in your activity layout.xml. + + + +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 + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + findViewById(R.id.media_route_button)?.also { + val unused = MediaRouteButtonFactory.setUpMediaRouteButton(context, it) + } +} + +``` + +### Java + +```java +@Override +public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // ... + MediaRouteButton button = findViewById(R.id.media_route_button); + 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`: + +
+ +### Kotlin + +```kotlin +private val playerListener: Player.Listener = + object : Player.Listener { + override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) { + // Add UI changes for local playback. + } else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) { + // Add UI changes for remote playback. + } + } + } + +override fun onStart() { + super.onStart() + mediaController.addListener(playerListener) +} + +override fun onStop() { + super.onStop() + mediaController.removeListener(playerListener) +} + +``` + +### Java + +```java +private final Player.Listener playerListener = + new Player.Listener() { + @Override + public void onDeviceInfoChanged(DeviceInfo deviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) { + // Add UI changes for local playback. + } else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) { + // Add UI changes for remote playback. + } + } + }; + +@Override +protected void onStart() { + super.onStart(); + mediaController.addListener(playerListener); +} + +@Override +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 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 new file mode 100644 index 0000000..caf237a --- /dev/null +++ b/media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md @@ -0,0 +1,88 @@ +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: + + + ... + + ... + + +## Create a custom options provider + +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. + + + ... + + ... + + +### 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. + +For more information, see [CastOptions.Builder](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder). + +
+ +### Kotlin + +```kotlin +class MyCustomCastOptionsProvider : OptionsProvider { + + override fun getCastOptions(context: Context): CastOptions { + return CastOptions.Builder() + .setReceiverApplicationId(APP_ID) + .setRemoteToLocalEnabled(true) + .build() + } + + override fun getAdditionalSessionProviders(context: Context): List? { + return null + } + + companion object { + // Add your receiver app ID in . + private const val APP_ID = "" + } +} + +``` + +### Java + +```java +public static final class MyCustomCastOptionsProvider implements OptionsProvider { + + // Add your receiver app ID in . + public static final String APP_ID = ""; + + @Override + public CastOptions getCastOptions(Context context) { + return new CastOptions.Builder() + .setReceiverApplicationId(APP_ID) + .setRemoteToLocalEnabled(true) + .build(); + } + + @Override + @Nullable + public List getAdditionalSessionProviders(Context context) { + 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 new file mode 100644 index 0000000..1a38415 --- /dev/null +++ b/media/media3-cast-integration/references/android/media/media3/cast/index.md @@ -0,0 +1,13 @@ +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. + +[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. + +[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 4957098..a7029fe 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-07-18' + last-updated: '2026-08-06' keywords: - recipe - Android @@ -63,8 +63,10 @@ Code examples showcasing common patterns. ### Deep links -- *[Basic](references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md)*: Shows how to parse a deep link URL from an Android Intent into a navigation key. -- *[Advanced](references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md)*: Shows how to handle deep links with a synthetic back stack and correct "Up" navigation behavior. +- *[Static URI](references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md)*: Shows how to handle simple static URI deep links. +- *[URI with Arguments](references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md)*: Shows how to parse path and query arguments from a deep link. +- *[Synthetic BackStack](references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md)*: Shows how to handle deep links with a synthetic back stack. +- *[Custom Matcher](references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md)*: Shows how to implement custom deep link matching logic. ### Scenes @@ -86,6 +88,7 @@ Code examples showcasing common patterns. ### Animations - *[Animations](references/android/guide/navigation/navigation-3/recipes/animations.md)*: Shows how to override the default animations for all destinations and a single destination. +- *[Conditional Transitions](references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md)*: Shows how to implement conditional transition animations. ### Common back stack behavior @@ -95,6 +98,10 @@ Code examples showcasing common patterns. - *[Conditional navigation](references/android/guide/navigation/navigation-3/recipes/conditional.md)*: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding. +### Lifecycle + +- *[Lifecycle Owner](references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md)*: Shows how to use and observe Lifecycle in Navigation 3. + ### Architecture - *[Modularized navigation code (Hilt)](references/android/guide/navigation/navigation-3/recipes/modular-hilt.md)*: Demonstrates how to decouple navigation code into separate modules using Hilt or Dagger for DI. 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 b4cefbf..66f2297 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,7 +1,18 @@ -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: +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3) + +### Jetpack Navigation 3 + +Use an Android skill to help you build using Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill navigation-3 + +
+ +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) @@ -11,28 +22,23 @@ creates a flexible app navigation system by providing: 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 ba86f3c..970e3da 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,3 +1,17 @@ +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3) + +### Jetpack Navigation 3 + +Use an Android skill to help you build and migrate to Jetpack Navigation 3. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill navigation-3 + +
+ To migrate your app from [Navigation 2](https://developer.android.com/guide/navigation) to Navigation 3, follow these steps: 1. Add the Navigation 3 dependencies. @@ -8,46 +22,16 @@ To migrate your app from [Navigation 2](https://developer.android.com/guide/navi 6. Replace `NavHost` with `NavDisplay`. 7. Remove Navigation 2 dependencies. -> [!IMPORTANT] -> **Important:** We released an agent skill to help you install and migrate to Jetpack Navigation 3. Try out the skill from the [Android skills repository](https://github.com/android/skills). - -
- - -## AI Prompt - -### Migrate from Navigation 2 to Navigation 3 - -This prompt will use this guide to migrate to navigation 3. - - Migrate from Navigation 2 to Navigation 3 using the official - migration guide. - -### Using AI prompts - -AI prompts are intended to be used within Gemini in Android Studio. - -Learn more about Gemini in Studio here: [https://developer.android.com/studio/gemini/overview](https://developer.android.com/studio/gemini/overview) - - -
- -If you run into problems [file an issue here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D). - ## 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 @@ -59,9 +43,7 @@ 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 @@ -72,41 +54,31 @@ 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** @@ -134,14 +106,11 @@ project. The core dependencies are provided for you to copy. 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: @@ -158,8 +127,7 @@ 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 @@ -253,19 +221,13 @@ name to match your project structure. .toMutableStateList() } -**AI Agent** : `rememberSerializable` is correct. Do not change it to -`rememberSaveable`. +**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. +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 @@ -313,8 +275,7 @@ 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 = , @@ -340,8 +301,7 @@ 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: @@ -356,22 +316,36 @@ After: val isSelected = key == navigationState.topLevelRoute -Verify that you have removed all references to `NavController`, including -any imports. +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 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`. + +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) + +After: + + // 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. @@ -379,35 +353,26 @@ as follows: > [!NOTE] > **Note:** If your app needs to navigate from an entry in one stack to another, you need to define the parent-child relationships for the routes and update the navigation logic in `Navigator` to support this. -## Step 5.1: Create an `entryProvider` +### 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 { } -## Step 5.2: Move destinations into the `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. @@ -493,6 +458,4 @@ 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 5e15bd4..579a539 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,4 +144,6 @@ 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 087d5cf..4ab55aa 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,4 +86,6 @@ 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 2067c08..461afd7 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,4 +82,6 @@ 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 fd2a72d..4575a71 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,4 +87,6 @@ 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 aa6b456..1ae419a 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,6 +102,8 @@ class BottomSheetActivity : ComponentActivity() { } } } + + ``` ``` @@ -192,4 +194,5 @@ 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 d49b399..da4f696 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,4 +197,6 @@ 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 new file mode 100644 index 0000000..497f208 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md @@ -0,0 +1,184 @@ +# Conditional Transitions Recipe + +This recipe demonstrates how to create route-dependent screen transitions in Navigation 3 using `transitionSpec` and `popTransitionSpec`. The slide directions (right, left, bottom, top) are conditionally selected based on pairs of `initialState` and `targetState` navigation keys. + +## How it works + +- **Route Definitions** : Navigation destinations (`Step1`, `Step2`, `Step3`, `Step4`) are defined using a sealed class hierarchy implementing `NavKey` and marked with `@Serializable`. +- **Conditional Forward Transitions (`transitionSpec`)** : Matches pairs of `(initialKey to targetKey)` to determine the direction of the slide animation: + - `Step1` $\\rightarrow$ `Step2`: Swipes to the left + - `Step2` $\\rightarrow$ `Step3`: Swipes to the up + - `Step3` $\\rightarrow$ `Step4`: Swipes to the right + - `Step4` $\\rightarrow$ `Step1`: Slides to the bottom (restarts flow) +- **Conditional Pop Transitions (`popTransitionSpec`)**: Handles reverse slide directions when navigating back or when clearing the backstack. +- **Backstack Control** : Demonstrates clearing the navigation stack on the final step (`backStack.clear()` \& `backStack.add(Step1)`) while executing a seamless top-slide transition. + +[![](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/conditionaltransitions) + +``` +package com.example.nav3recipes.conditionaltransitions + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.content.ContentOrange +import com.example.nav3recipes.content.ContentPurple +import com.example.nav3recipes.content.ContentRed +import com.example.nav3recipes.sharedviewmodel.toContentKey +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.serialization.Serializable + +@Serializable +sealed class Step(val firstButtonTitle: String, val secondButtonTitle: String) : NavKey + +@Serializable +data object Step1 : Step("Home", "Swipe left") + +@Serializable +data object Step2 : Step("Swipe right", "Swipe up") + +@Serializable +data object Step3 : Step("Swipe down", "Swipe right") + +@Serializable +data object Step4 : Step("Swipe left", "Swipe down") + +@AndroidEntryPoint +class ConditionalTransitionsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setEdgeToEdgeConfig() + + setContent { + Scaffold { paddingValues -> + val backStack = rememberNavBackStack(Step1) + + NavDisplay( + backStack = backStack, + modifier = Modifier.padding(paddingValues), + onBack = backStack::removeLastOrNull, + entryProvider = entryProvider { + entry { + ContentGreen(title = "Screen 1") { + StepContent( + step = it, + onNext = { backStack += Step2 }, + onBack = ::finish, // closing the activity + ) + } + + } + entry { + ContentRed(title = "Screen 2") { + StepContent( + step = it, + onNext = { backStack += Step3 }, + onBack = backStack::removeLastOrNull, + ) + } + } + entry { + ContentOrange(title = "Screen 3") { + StepContent( + step = it, + onNext = { backStack += Step4 }, + onBack = backStack::removeLastOrNull, + ) + } + } + entry { + ContentPurple(title = "Screen 4") { + StepContent( + step = it, + onNext = { + backStack.clear() + backStack.add(Step1) + }, + onBack = backStack::removeLastOrNull, + ) + } + } + }, + transitionSpec = { + val initialKey = initialState.entries.lastOrNull()?.contentKey + val targetKey = targetState.entries.lastOrNull()?.contentKey + + when (initialKey to targetKey) { + Step1.toContentKey() to Step2.toContentKey() -> swipeLeft() + Step2.toContentKey() to Step3.toContentKey() -> swipeUp() + Step3.toContentKey() to Step4.toContentKey() -> swipeRight() + Step4.toContentKey() to Step1.toContentKey() -> swipeDown() + else -> swipeRight() + } + }, + popTransitionSpec = { + val initialKey = initialState.entries.lastOrNull()?.contentKey + val targetKey = targetState.entries.lastOrNull()?.contentKey + + when (initialKey to targetKey) { + Step4.toContentKey() to Step1.toContentKey() -> swipeDown() // via backstack clearing + Step2.toContentKey() to Step1.toContentKey() -> swipeRight() + Step3.toContentKey() to Step2.toContentKey() -> swipeDown() + Step4.toContentKey() to Step3.toContentKey() -> swipeLeft() + else -> swipeLeft() + } + }, + ) + } + } + } +} + +@Composable +fun StepContent(step: Step, onNext: () -> Unit, onBack: () -> Unit) { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { + ElevatedButton(onBack) { Text(text = step.firstButtonTitle) } + ElevatedButton(onNext) { Text(text = step.secondButtonTitle) } + } + } +} + +private fun swipeLeft(): ContentTransform = + slideInHorizontally(initialOffsetX = { it }) togetherWith slideOutHorizontally(targetOffsetX = { -it }) + +private fun swipeUp(): ContentTransform = + slideInVertically(initialOffsetY = { it }) togetherWith slideOutVertically(targetOffsetY = { -it }) + +private fun swipeRight(): ContentTransform = + slideInHorizontally(initialOffsetX = { -it }) togetherWith slideOutHorizontally(targetOffsetX = { it }) + +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 60848b3..0bd6bca 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,6 +176,7 @@ fun rememberNavBackStack(vararg elements: T): NavBackStack { NavBackStack(*elements) } } + ``` ``` @@ -227,4 +228,5 @@ 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-basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md deleted file mode 100644 index 66312de..0000000 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-basic.md +++ /dev/null @@ -1,744 +0,0 @@ -# Deep Link Basic Recipe - -This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. - -## How it works - -It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink request, and the `MainActivity` to show how an app can handle that request. - -## Demonstrated forms of deeplink - -The `MainActivity` has several backStack keys to demonstrate different types of supported deeplinks: - -1. `HomeKey` - deeplink with an exact url (no deeplink arguments) -2. `UsersKey` - deeplink with path arguments -3. `SearchKey` - deeplink with query arguments - -See `MainActivity.deepLinkPatterns` for the actual url pattern of each. - -## Recipe structure - -This recipe consists of three main packages: - -1. `basic.deeplink` - Contains the two activities -2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc -3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks - -[![](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/basic) - -``` -package com.example.nav3recipes.deeplink.basic - -import androidx.navigation3.runtime.NavKey -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS -import kotlinx.serialization.Serializable - -internal interface NavRecipeKey: NavKey { - val name: String -} - -@Serializable -internal object HomeKey: NavRecipeKey { - override val name: String = STRING_LITERAL_HOME -} - -@Serializable -internal data class UsersKey( - val filter: String, -): NavRecipeKey { - override val name: String = STRING_LITERAL_USERS - companion object { - const val FILTER_KEY = STRING_LITERAL_FILTER - const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" - const val FILTER_OPTION_ALL = "all" - } -} - -@Serializable -internal data class SearchKey( - val firstName: String? = null, - val ageMin: Int? = null, - val ageMax: Int? = null, - val location: String? = null, -): NavRecipeKey { - override val name: String = STRING_LITERAL_SEARCH -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.core.net.toUri -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FriendsList -import com.example.nav3recipes.common.deeplink.LIST_USERS -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT -import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult -import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatcher -import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern -import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest -import com.example.nav3recipes.deeplink.basic.util.KeyDecoder -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * Parses a target deeplink into a NavKey. There are several crucial steps involved: - * - * STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable - * format (see [DeepLinkPattern]) - * STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkRequest]) - * **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each - * other to facilitate comparison and finding a match - * STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match - * (see [DeepLinkMatchResult]). The match result's format should enable conversion from result - * to backstack key, regardless of what the conversion method may be. - * STEP 4. Associate the match results with the correct backstack key - * - * This recipes provides an example for each of the above steps by way of kotlinx.serialization. - * - * **This recipe is designed to focus on parsing an intent into a key, and therefore these additional - * deeplink considerations are not included in this scope** - * - Create synthetic backStack - * - Multi-modular setup - * - DI - * - Managing TaskStack - * - Up button ves Back Button - * - */ -class MainActivity : ComponentActivity() { - /** STEP 1. Parse supported deeplinks */ - // internal so that landing activity can link to this in the kdocs - internal val deepLinkPatterns: List> = listOf( - // "https://www.nav3recipes.com/home" - DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), - // "https://www.nav3recipes.com/users/with/{filter}" - DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), - // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" - DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), - ) - - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - // retrieve the target Uri - val uri: Uri? = intent.data - // associate the target with the correct backstack key - val key: NavKey = uri?.let { - /** STEP 2. Parse requested deeplink */ - val request = DeepLinkRequest(uri) - /** STEP 3. Compared requested with supported deeplink to find match*/ - val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> - DeepLinkMatcher(request, pattern).match() - } - /** STEP 4. If match is found, associate match to the correct key*/ - match?.let { - //leverage kotlinx.serialization's Decoder to decode - // match result into a backstack key - KeyDecoder(match.args) - .decodeSerializableValue(match.serializer) - } - } ?: HomeKey // fallback if intent.uri is null or match is not found - - /** - * Then pass starting key to backstack - */ - setContent { - val backStack: NavBackStack = rememberNavBackStack(key) - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { key -> - EntryScreen(key.name) { - TextContent("") - } - } - entry { key -> - EntryScreen("${key.name} : ${key.filter}") { - TextContent("") - val list = when { - key.filter.isEmpty() -> LIST_USERS - key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS - else -> LIST_USERS.take(5) - } - FriendsList(list) - } - } - entry { search -> - EntryScreen(search.name) { - TextContent("") - val matchingUsers = LIST_USERS.filter { user -> - (search.firstName == null || user.firstName == search.firstName) && - (search.location == null || user.location == search.location) && - (search.ageMin == null || user.age >= search.ageMin) && - (search.ageMax == null || user.age <= search.ageMax) - } - FriendsList(matchingUsers) - } - } - } - ) - } - } -} -``` - -``` -package com.example.nav3recipes.deeplink.basic - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.core.net.toUri -import androidx.lifecycle.compose.dropUnlessResumed -import com.example.nav3recipes.common.deeplink.EMPTY -import com.example.nav3recipes.common.deeplink.EntryScreen -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN -import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE -import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY -import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM -import com.example.nav3recipes.common.deeplink.LOCATION_BC -import com.example.nav3recipes.common.deeplink.LOCATION_BR -import com.example.nav3recipes.common.deeplink.LOCATION_CA -import com.example.nav3recipes.common.deeplink.LOCATION_US -import com.example.nav3recipes.common.deeplink.MenuDropDown -import com.example.nav3recipes.common.deeplink.MenuTextInput -import com.example.nav3recipes.common.deeplink.PaddedButton -import com.example.nav3recipes.common.deeplink.TextContent -import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE -import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE -import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH -import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME -import com.example.nav3recipes.ui.setEdgeToEdgeConfig - -/** - * This activity allows the user to create a deep link and make a request with it. - * - * **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to construct - * and trigger the deeplink request, and the [MainActivity] to show how an app can handle - * that request. - * - * **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack keys to - * demonstrate different types of supported deeplinks: - * 1. [HomeKey] - deeplink with an exact url (no deeplink arguments) - * 2. [UsersKey] - deeplink with path arguments - * 3. [SearchKey] - deeplink with query arguments - * See [MainActivity.deepLinkPatterns] for the actual url pattern of each. - * - * **RECIPE STRUCTURE** This recipe consists of three main packages: - * 1. basic.deeplink - Contains the two activities - * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc - * 3. basic.deeplink.util - Contains the classes and helper methods to parse and match - * the deeplinks - * - * See [MainActivity] for how the requested deeplink is handled. - */ -class CreateDeepLinkActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setEdgeToEdgeConfig() - super.onCreate(savedInstanceState) - - setContent { - /** - * UI for deeplink sandbox - */ - EntryScreen("Sandbox - Build Your Deeplink") { - TextContent("Base url:\n${PATH_BASE}/") - var showFilterOptions by remember { mutableStateOf(false) } - val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } - - var showQueryOptions by remember { mutableStateOf(false) } - var selectedFilter by remember { mutableStateOf("") } - val selectedSearchQuery = remember { mutableStateMapOf() } - - // manage path options - MenuDropDown( - menuOptions = MENU_OPTIONS_PATH, - ) { _, selection -> - selectedPath.value = selection - when (selection) { - PATH_SEARCH -> { - showQueryOptions = true - showFilterOptions = false - } - - PATH_INCLUDE -> { - showQueryOptions = false - showFilterOptions = true - } - - else -> { - showQueryOptions = false - showFilterOptions = false - } - } - } - - // manage path filter options, reset state if menu is closed - LaunchedEffect(showFilterOptions) { - selectedFilter = if (showFilterOptions) { - MENU_OPTIONS_FILTER.values.first().first() - } else { - "" - } - } - if (showFilterOptions) { - MenuDropDown( - menuOptions = MENU_OPTIONS_FILTER, - ) { _, selected -> - selectedFilter = selected - } - } - - // manage query options, reset state if menu is closed - LaunchedEffect(showQueryOptions) { - if (showQueryOptions) { - val initEntry = MENU_OPTIONS_SEARCH.entries.first() - selectedSearchQuery[initEntry.key] = initEntry.value.first() - } else { - selectedSearchQuery.clear() - } - } - if (showQueryOptions) { - MenuTextInput( - menuLabels = MENU_LABELS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - MenuDropDown( - menuOptions = MENU_OPTIONS_SEARCH, - ) { label, selected -> - selectedSearchQuery[label] = selected - } - } - - // form final deeplink url - val arguments = when (selectedPath.value) { - PATH_INCLUDE -> "/${selectedFilter}" - PATH_SEARCH -> { - buildString { - selectedSearchQuery.forEach { entry -> - if (entry.value.isNotEmpty()) { - val prefix = if (isEmpty()) "?" else "&" - append("$prefix${entry.key}=${entry.value}") - } - } - } - } - - else -> "" - } - val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" - TextContent("Final url:\n$finalUrl") - // deeplink to target - PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { - val intent = Intent( - this@CreateDeepLinkActivity, - MainActivity::class.java - ) - // start activity with the url - intent.data = finalUrl.toUri() - startActivity(intent) - }) - } - } - } -} - -private const val KEY_PATH = "path" -private val MENU_OPTIONS_PATH = mapOf( - KEY_PATH to listOf( - STRING_LITERAL_HOME, - PATH_INCLUDE, - PATH_SEARCH, - ), -) - -private val MENU_OPTIONS_FILTER = mapOf( - UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), -) - -private val MENU_OPTIONS_SEARCH = mapOf( - SearchKey::firstName.name to listOf( - EMPTY, - FIRST_NAME_JOHN, - FIRST_NAME_TOM, - FIRST_NAME_MARY, - FIRST_NAME_JULIE - ), - SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) -) - -private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) - -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -/** - * Decodes the list of arguments into a a back stack key - * - * **IMPORTANT** This decoder assumes that all argument types are Primitives. - */ -@OptIn(ExperimentalSerializationApi::class) -internal class KeyDecoder( - private val arguments: Map, -) : AbstractDecoder() { - - override val serializersModule: SerializersModule = EmptySerializersModule() - private var elementIndex: Int = -1 - private var elementName: String = "" - - /** - * Decodes the index of the next element to be decoded. Index represents a position of the - * current element in the [descriptor] that can be found with [descriptor].getElementIndex. - * - * The returned index will trigger deserializer to call [decodeValue] on the argument at that - * index. - * - * The decoder continually calls this method to process the next available argument until this - * method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no more - * arguments to decode. - * - * This method should sequentially return the element index for every element that has its value - * available within [arguments]. - */ - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - var currentIndex = elementIndex - while (true) { - // proceed to next element - currentIndex++ - // if we have reached the end, let decoder know there are not more arguments to decode - if (currentIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE - val currentName = descriptor.getElementName(currentIndex) - // Check if bundle has argument value. If so, we tell decoder to process - // currentIndex. Otherwise, we skip this index and proceed to next index. - if (arguments.contains(currentName)) { - elementIndex = currentIndex - elementName = currentName - return elementIndex - } - } - } - - /** - * Returns argument value from the [arguments] for the argument at the index returned by - * [decodeElementIndex] - */ - override fun decodeValue(): Any { - val arg = arguments[elementName] - checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName" } - return arg - } - - override fun decodeNull(): Nothing? = null - - // we want to know if it is not null, so its !isNull - override fun decodeNotNullMark(): Boolean = arguments[elementName] != null -} -``` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri - -/** - * Parse the requested Uri and store it in a easily readable format - * - * @param uri the target deeplink uri to link to - */ -internal class DeepLinkRequest( - val uri: Uri -) { - /** - * A list of path segments - */ - val pathSegments: List = uri.pathSegments - - /** - * A map of query name to query value - */ - val queries = buildMap { - uri.queryParameterNames.forEach { argName -> - this[argName] = uri.getQueryParameter(argName)!! - } - } - - // TODO add parsing for other Uri components, i.e. fragments, mimeType, action -} -``` - -```` -package com.example.nav3recipes.deeplink.basic.util - -import android.net.Uri -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.encoding.CompositeDecoder -import java.io.Serializable - -/** - * Parse a supported deeplink and stores its metadata as a easily readable format - * - * The following notes applies specifically to this particular sample implementation: - * - * The supported deeplink is expected to be built from a serializable backstack key [T] that - * supports deeplink. This means that if this deeplink contains any arguments (path or query), - * the argument name must match any of [T] member field name. - * - * One [DeepLinkPattern] should be created for each supported deeplink. This means if [T] - * supports two deeplink patterns: - * ``` - * val deeplink1 = www.nav3recipes.com/home - * val deeplink2 = www.nav3recipes.com/profile/{userId} - * ``` - * Then two [DeepLinkPattern] should be created - * ``` - * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1) - * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), deeplink2) - * ``` - * - * This implementation assumes a few things: - * 1. all path arguments are required/non-nullable - partial path matches will be considered a non-match - * 2. all query arguments are optional by way of nullable/has default value - * - * @param T the backstack key type that supports the deeplinking of [uriPattern] - * @param serializer the serializer of [T] - * @param uriPattern the supported deeplink's uri pattern, i.e. "abc.com/home/{pathArg}" - */ -internal class DeepLinkPattern( - val serializer: KSerializer, - val uriPattern: Uri -) { - /** - * Help differentiate if a path segment is an argument or a static value - */ - private val regexPatternFillIn = Regex("\\{(.+?)\\}") - - // TODO make these lazy - /** - * parse the path into a list of [PathSegment] - * - * order matters here - path segments need to match in value and order when matching - * requested deeplink to supported deeplink - */ - val pathSegments: List = buildList { - uriPattern.pathSegments.forEach { segment -> - // first, check if it is a path arg - var result = regexPatternFillIn.find(segment) - if (result != null) { - // if so, extract the path arg name (the string value within the curly braces) - val argName = result.groups[1]!!.value - // from [T], read the primitive type of this argument to get the correct type parser - val elementIndex = serializer.descriptor.getElementIndex(argName) - if (elementIndex == CompositeDecoder.UNKNOWN_NAME) { - throw IllegalArgumentException( - "Path parameter '{$argName}' defined in the DeepLink $uriPattern does not exist in the Serializable class '${serializer.descriptor.serialName}'." - ) - } - - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - // finally, add the arg name and its respective type parser to the map - add(PathSegment(argName, true, getTypeParser(elementDescriptor.kind))) - } else { - // if its not a path arg, then its just a static string path segment - add(PathSegment(segment, false, getTypeParser(PrimitiveKind.STRING))) - } - } - } - - /** - * Parse supported queries into a map of queryParameterNames to [TypeParser] - * - * This will be used later on to parse a provided query value into the correct KType - */ - val queryValueParsers: Map = buildMap { - uriPattern.queryParameterNames.forEach { paramName -> - val elementIndex = serializer.descriptor.getElementIndex(paramName) - // Ignore static query parameters that are not in the Serializable class - if (elementIndex != CompositeDecoder.UNKNOWN_NAME) { - val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex) - this[paramName] = getTypeParser(elementDescriptor.kind) - } - } - } - - /** - * Metadata about a supported path segment - */ - class PathSegment( - val stringValue: String, - val isParamArg: Boolean, - val typeParser: TypeParser - ) -} - -/** - * Parses a String into a Serializable Primitive - */ -private typealias TypeParser = (String) -> Serializable - -private fun getTypeParser(kind: SerialKind): TypeParser { - return when (kind) { - PrimitiveKind.STRING -> Any::toString - PrimitiveKind.INT -> String::toInt - PrimitiveKind.BOOLEAN -> String::toBoolean - PrimitiveKind.BYTE -> String::toByte - PrimitiveKind.CHAR -> String::toCharArray - PrimitiveKind.DOUBLE -> String::toDouble - PrimitiveKind.FLOAT -> String::toFloat - PrimitiveKind.LONG -> String::toLong - PrimitiveKind.SHORT -> String::toShort - else -> throw IllegalArgumentException( - "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive." - ) - } -} -```` - -``` -package com.example.nav3recipes.deeplink.basic.util - -import android.util.Log -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.KSerializer - -internal class DeepLinkMatcher( - val request: DeepLinkRequest, - val deepLinkPattern: DeepLinkPattern -) { - /** - * Match a [DeepLinkRequest] to a [DeepLinkPattern]. - * - * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise - */ - fun match(): DeepLinkMatchResult? { - if (request.uri.scheme != deepLinkPattern.uriPattern.scheme) return null - if (!request.uri.authority.equals(deepLinkPattern.uriPattern.authority, ignoreCase = true)) return null - if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null - // exact match (url does not contain any arguments) - if (request.uri == deepLinkPattern.uriPattern) - return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf()) - - val args = mutableMapOf() - // match the path - request.pathSegments - .asSequence() - // zip to compare the two objects side by side, order matters here so we - // need to make sure the compared segments are at the same position within the url - .zip(deepLinkPattern.pathSegments.asSequence()) - .forEach { it -> - // retrieve the two path segments to compare - val requestedSegment = it.first - val candidateSegment = it.second - // if the potential match expects a path arg for this segment, try to parse the - // requested segment into the expected type - if (candidateSegment.isParamArg) { - val parsedValue = try { - candidateSegment.typeParser.invoke(requestedSegment) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e) - return null - } - args[candidateSegment.stringValue] = parsedValue - } else if(requestedSegment != candidateSegment.stringValue){ - // if it's path arg is not the expected type, its not a match - return null - } - } - // match queries (if any) - request.queries.forEach { query -> - val name = query.key - // If the pattern does not define this query parameter, ignore it. - // This prevents a NullPointerException. - val queryStringParser = deepLinkPattern.queryValueParsers[name]?: return@forEach - - val queryParsedValue = try { - queryStringParser.invoke(query.value) - } catch (e: IllegalArgumentException) { - Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e) - return null - } - args[name] = queryParsedValue - } - // provide the serializer of the matching key and map of arg names to parsed arg values - return DeepLinkMatchResult(deepLinkPattern.serializer, args) - } -} - - -/** - * Created when a requested deeplink matches with a supported deeplink - * - * @param [T] the backstack key associated with the deeplink that matched with the requested deeplink - * @param serializer serializer for [T] - * @param args The map of argument name to argument value. The value is expected to have already - * been parsed from the raw url string back into its proper KType as declared in [T]. - * Includes arguments for all parts of the uri - path, query, etc. - * */ -internal data class DeepLinkMatchResult( - val serializer: KSerializer, - val args: Map -) - -const val TAG_LOG_ERROR = "Nav3RecipesDeepLink" -``` - -``` -package com.example.nav3recipes.deeplink.basic.ui - -import com.example.nav3recipes.deeplink.basic.SearchKey - -/** - * String resources - */ -internal const val STRING_LITERAL_FILTER = "filter" -internal const val STRING_LITERAL_HOME = "home" -internal const val STRING_LITERAL_USERS = "users" -internal const val STRING_LITERAL_SEARCH = "search" -internal const val STRING_LITERAL_INCLUDE = "include" -internal const val PATH_BASE = "https://www.nav3recipes.com" -internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" -internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" -internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" - -internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" -internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + - "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + - "&${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/deeplinks-custommatcher.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md new file mode 100644 index 0000000..cf2d0c1 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md @@ -0,0 +1,178 @@ +# Custom DeepLinkMatcher Recipe + +This recipe demonstrates how to create a custom `DeepLinkMatcher` in Navigation 3 using custom request extras and Kotlinx Serialization. + +## How it works + +This recipe consists of two activities: + +- `CustomDeepLinkMatcherActivity`: Accepts user input, serializes a `HomeKey` instance into JSON, attaches it to an `Intent` extra via a `RequestExtrasKey`, and launches `MainActivity`. +- `MainActivity`: Constructs a `DeepLinkRequest(intent)`, evaluates it with `JsonDeepLinkMatcher`, decodes the `HomeKey`, and sets it as the starting route in `NavDisplay`. + +## 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`. + +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) + +``` +package com.example.nav3recipes.deeplink.usecases.matcher + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.RequestExtrasKey +import androidx.navigation3.runtime.deeplink.get +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +class CustomDeepLinkMatcherActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + EntryScreen { + Column( + verticalArrangement = Arrangement.spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var text by remember { mutableStateOf("") } + OutlinedTextField( + placeholder = { Text("Your name...", color = Color.Black.copy(alpha = 0.5f)) }, + value = text, + singleLine = true, + onValueChange = { text = it }, + ) + + ElevatedButton( + onClick = + dropUnlessResumed { + val intent = Intent( + this@CustomDeepLinkMatcherActivity, + MainActivity::class.java + ) + val json = Json.encodeToString(HomeKey.serializer(), HomeKey(text)) + intent.putExtra(JsonDeepLinkMatcherKey.toString(), json) + startActivity(intent) + } + ) { + Text("Sign up") + } + } + } + } + } +} + +internal data object JsonDeepLinkMatcherKey: RequestExtrasKey + +internal class JsonDeepLinkMatcher(val serializer: KSerializer): DeepLinkMatcher>() { + override fun matchRequest(request: DeepLinkRequest): MatchResult? { + val json = request.extras[JsonDeepLinkMatcherKey] ?: return null + return try { + val result = Json.decodeFromString(serializer, json) + MatchResult(result) + } catch (e: SerializationException) { + Log.v("DeepLinkMatcher", "Failed to decode json", e) + return null + } + } +} + + +``` + +``` +package com.example.nav3recipes.deeplink.usecases.matcher + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.invoke +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +@Serializable +internal data class HomeKey(val name: String): NavKey + +@Serializable +internal object FallbackKey: NavKey + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + val request = DeepLinkRequest(intent) + val deepLinkMatcher = createJsonDeepLinkMatcher() + + val matchResult = deepLinkMatcher.match(request) + val key = matchResult?.key ?: FallbackKey + + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = backStack::removeLastOrNull, + entryProvider = entryProvider { + entry { key -> + EntryScreen("Welcome") { + TextContent(key.name) + } + } + entry { key -> + EntryScreen("Fallback Key") { + TextContent( + "Failed to deep link - DeepLinkRequest " + + "did not match with any DeepLinkMatcher" + ) + } + } + } + ) + } + } +} + +// Optional JsonDeepLinkMatcher factory function that automatically captures KSerializer for T. +private inline fun createJsonDeepLinkMatcher(): JsonDeepLinkMatcher { + 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 new file mode 100644 index 0000000..5b9aa71 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md @@ -0,0 +1,143 @@ +# Deep Link Static URI Recipe + +This recipe demonstrates how deep link with a static Uri. + +## Recipe components + +The recipe contains two activities: + +1. `StaticUriDeepLinkActivity` to construct and start an Intent with the deep link uri +2. `MainActivity` is the target Activity of the deep link, represents an app that users can deep link to. + +## How the demonstrated deep link works + +1. The deep link source (`StaticUriDeepLinkActivity`) defines the uri and creates an Intent to deep link with. +2. The app (`MainActivity`) declares a navigation key (`HomeKey`). To indicate that `HomeKey` supports deep linking, the app declares a `UriDeepLinkMatcher` with the `HomeKey` serializer along with the uri pattern that `HomeKey` supports. +3. `MainActivity` onCreate instantiates a `DeepLinkRequest` with the intent and matches it with the `UriDeepLinkMatcher` to get a `MatchResult`. If the `MatchResult` is non-null, the app navigates to the key returned by the result. Otherwise, the deep link is not supported and the app navigates to a `Fallback` screen. + +[![](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/staticuri) + +``` +package com.example.nav3recipes.deeplink.handlerequests.staticuri + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_BASE +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +const val HOME_URI = "$PATH_BASE/home" + +class StaticUriDeepLinkActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + EntryScreen("Deep link url:") { + TextContent(HOME_URI) + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent( + this@StaticUriDeepLinkActivity, + MainActivity::class.java + ) + // the uri to deep link with + intent.data = HOME_URI.toUri() + startActivity(intent) + }) + } + } + } +} + + +``` + +``` +package com.example.nav3recipes.deeplink.handlerequests.staticuri + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.HomeKey +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.NavRecipeKey +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.invoke +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + + +@Serializable +internal object FallbackKey: NavRecipeKey { + override val name: String = "Fallback Key" +} + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + // create a DeepLinkRequest with the intent + val request = DeepLinkRequest(intent) + + // try to match DeepLinkRequest to a DeepLinkMatcher + val matchResult = HOME_MATCHER.match(request) + val key = matchResult?.key ?: FallbackKey + + /** + * Then pass starting key to backstack + */ + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { key -> + EntryScreen(key.name) { + TextContent("Deep linked to Home") + } + } + entry { key -> + EntryScreen("${key.name} ") { + TextContent( + "Failed to deep link - DeepLinkRequest " + + "did not match with any DeepLinkMatcher" + ) + } + } + + } + ) + } + } +} + +/** + * Each matcher is associated with a navigation key that supports this deep link. + * + * A navigation key can be associated with multiple DeepLinkMatchers if it supports more than one deep link. + */ +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-advanced.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md similarity index 62% rename from navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md rename to navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md index f241291..6eae0e3 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-advanced.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md @@ -1,34 +1,54 @@ -# Deep Link Advanced Recipe +# 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" deeplinks -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 [com.example.nav3recipes.deeplink.advanced](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/advanced), which -contains the `CreateAdvancedDeepLinkActivity` 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 [advanceddeeplinkapp](https://developer.android.com/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced), which contains -the MainActivity 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. + +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`. + +# How it Works + +The recipe follows the deep link guideline summarized [here](https://developer.android.com/docs/deeplink-guide#summary). + +To see behavior of `Existing Task`: + +1. Open deep link using current task +2. On the device, swipe up to see all recent apps +3. Notice that the new Activity is opened within the Nav3Recipes app +4. Click back button to go back to the original Activity +5. Repeat step 1 +6. Click the up button to go to parent screen +7. On the device, swipe up to see all recent apps +8. Notice that the new Activity is now opened within the Nav3SyntheticBackStack app + +To see behavior of `New Task`: + +1. Open deep link using new task +2. On the device, swipe up to see all recent apps +3. Notice that the new Activity is opened within the Nav3SyntheticBackStack app +4. Click Up or Back button to go to parent screen # Core implementation -The core helper functions for navigateUp and building synthetic backStack can be -found [here](https://developer.android.com/static/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt) +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. -[![](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/advanced) +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) ``` -package com.example.nav3recipes.deeplink.advanced +package com.example.nav3recipes.deeplink.handlerequests.syntheticbackstack import android.content.Intent import android.os.Bundle @@ -53,20 +73,20 @@ internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com" /** * The recipe entry point that allows users to create a deep link and make a request with it. * - * **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deeplinks + * **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deep links * into "App B". * - * "App A" is simulated by this current module [com.example.nav3recipes.deeplink.advanced], which - * contains the [AdvancedCreateDeepLinkActivity] that allows you to create a deeplink intent and + * "App A" is simulated by this current module `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 [com.example.nav3recipes.deeplink.advanced], which contains - * the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack + * "App B" is simulated by the module `syntheticbackstackapp`, 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. * * See the [README](README.md) file of current module for more info on advanced deep linking. */ -class AdvancedCreateDeepLinkActivity: ComponentActivity() { +class SyntheticBackStackDeepLinkActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() @@ -152,4 +172,6 @@ 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 new file mode 100644 index 0000000..5113ed5 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md @@ -0,0 +1,396 @@ +# Deep Link URI Arguments Recipe + +This recipe demonstrates how to parse a deep link URL from an Android Intent into a Navigation key. + +## Recipe components + +It consists of two activities + +1. `UriWithArgumentsDeepLinkActivity` constructs and triggers the deeplink request +2. `MainActivity` parses the intent into the target navigation key. + +## How it works + +The `MainActivity` handles the request with these steps + +1. Declare a `UriDeepLinkMatcher` for each url pattern that can be deep linked into. Each matcher accepts a uri pattern and the KSerializer of the NavKey that supports this deep link. + +2. Create a `DeepLinkRequest` with the incoming intent. + +3. Match all candidate `UriDeepLinkMatchers` with the request and compare the resulting `UriMatchResults` for the best match. + +4. Read the matching key from `UriMatchResult.key` or use default key if no match. + +This recipe focuses on handing an intent and does not include these considerations: + +- Create synthetic backStack +- Multi-modular setup +- DI +- Managing TaskStack +- Up button vs Back Button + +## Demonstrated forms of deeplink + +The `MainActivity` has several backStack keys to demonstrate different types of supported deep links: + +1. `HomeKey` - deeplink with an exact url (no deeplink arguments) +2. `UsersKey` - deeplink with path arguments +3. `SearchKey` - deeplink with query arguments + +See `MainActivity.deepLinkMatchers` for the actual url pattern of each. +[![](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/uriarguments) + +``` +package com.example.nav3recipes.deeplink.handlerequests.uriarguments + +import androidx.navigation3.runtime.NavKey +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_FILTER +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_SEARCH +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_USERS +import kotlinx.serialization.Serializable + +internal interface NavRecipeKey: NavKey { + val name: String +} + +@Serializable +internal object HomeKey: NavRecipeKey { + override val name: String = STRING_LITERAL_HOME +} + +@Serializable +internal data class UsersKey( + val filter: String, +): NavRecipeKey { + override val name: String = STRING_LITERAL_USERS + companion object { + const val FILTER_KEY = STRING_LITERAL_FILTER + const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" + const val FILTER_OPTION_ALL = "all" + } +} + +@Serializable +internal data class SearchKey( + val firstName: String? = null, + val ageMin: Int? = null, + val ageMax: Int? = null, + val location: String? = null, +): NavRecipeKey { + override val name: String = STRING_LITERAL_SEARCH +} + +``` + +``` +package com.example.nav3recipes.deeplink.handlerequests.uriarguments + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.net.toUri +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.invoke +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FriendsList +import com.example.nav3recipes.common.deeplink.LIST_USERS +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_HOME_EXACT +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_SEARCH +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_USERS_WITH_FILTER +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.serialization.serializer + +/** + * See README.md for how this recipe works. + */ +class MainActivity : ComponentActivity() { + /** STEP 1. Declare supported deep links */ + internal val deepLinkMatchers: List> = listOf( + // "https://www.nav3recipes.com/home" + UriDeepLinkMatcher(URL_HOME_EXACT.toUri(), serializer()), + // "https://www.nav3recipes.com/users/with/{filter}" + UriDeepLinkMatcher(URL_USERS_WITH_FILTER.toUri(), serializer()), + // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" + UriDeepLinkMatcher(URL_SEARCH.toUri(), serializer()), + ) + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + /** STEP 2. Create a [DeepLinkRequest] from the intent */ + val request = DeepLinkRequest(intent) + + /** STEP 3. Match the request to the DeepLinkMatchers*/ + // First get all the possible matching UriMatchResult + val matches = deepLinkMatchers.mapNotNull { + // returns null if no match + it.match(request) + } + // compare all matches to find best match + val bestMatch = matches.maxOrNull() + /** STEP 4. Get the key from the match or use default key if no match*/ + val key = bestMatch?.key ?: HomeKey + + /** + * STEP 5. pass the initial key to backstack + */ + setContent { + val backStack: NavBackStack = rememberNavBackStack(key) + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { key -> + EntryScreen(key.name) { + TextContent("") + } + } + entry { key -> + EntryScreen("${key.name} : ${key.filter}") { + TextContent("") + val list = when { + key.filter.isEmpty() -> LIST_USERS + key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS + else -> LIST_USERS.take(5) + } + FriendsList(list) + } + } + entry { search -> + EntryScreen(search.name) { + TextContent("") + val matchingUsers = LIST_USERS.filter { user -> + (search.firstName == null || user.firstName == search.firstName) && + (search.location == null || user.location == search.location) && + (search.ageMin == null || user.age >= search.ageMin) && + (search.ageMax == null || user.age <= search.ageMax) + } + FriendsList(matchingUsers) + } + } + } + ) + } + } +} + +``` + +``` +package com.example.nav3recipes.deeplink.handlerequests.uriarguments + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.compose.dropUnlessResumed +import com.example.nav3recipes.common.deeplink.EMPTY +import com.example.nav3recipes.common.deeplink.EntryScreen +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN +import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE +import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY +import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM +import com.example.nav3recipes.common.deeplink.LOCATION_BC +import com.example.nav3recipes.common.deeplink.LOCATION_BR +import com.example.nav3recipes.common.deeplink.LOCATION_CA +import com.example.nav3recipes.common.deeplink.LOCATION_US +import com.example.nav3recipes.common.deeplink.MenuDropDown +import com.example.nav3recipes.common.deeplink.MenuTextInput +import com.example.nav3recipes.common.deeplink.PaddedButton +import com.example.nav3recipes.common.deeplink.TextContent +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_BASE +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_INCLUDE +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_SEARCH +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME +import com.example.nav3recipes.ui.setEdgeToEdgeConfig + +/** + * See README.md for how this recipe works. + * + * See [MainActivity] for how the requested deeplink is handled. + */ +class UriWithArgumentsDeepLinkActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + + setContent { + /** + * UI for deeplink sandbox + */ + EntryScreen("Sandbox - Build Your Deeplink") { + TextContent("Base url:\n${PATH_BASE}/") + var showFilterOptions by remember { mutableStateOf(false) } + val selectedPath = remember { mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) } + + var showQueryOptions by remember { mutableStateOf(false) } + var selectedFilter by remember { mutableStateOf("") } + val selectedSearchQuery = remember { mutableStateMapOf() } + + // manage path options + MenuDropDown( + menuOptions = MENU_OPTIONS_PATH, + ) { _, selection -> + selectedPath.value = selection + when (selection) { + PATH_SEARCH -> { + showQueryOptions = true + showFilterOptions = false + } + + PATH_INCLUDE -> { + showQueryOptions = false + showFilterOptions = true + } + + else -> { + showQueryOptions = false + showFilterOptions = false + } + } + } + + // manage path filter options, reset state if menu is closed + LaunchedEffect(showFilterOptions) { + selectedFilter = if (showFilterOptions) { + MENU_OPTIONS_FILTER.values.first().first() + } else { + "" + } + } + if (showFilterOptions) { + MenuDropDown( + menuOptions = MENU_OPTIONS_FILTER, + ) { _, selected -> + selectedFilter = selected + } + } + + // manage query options, reset state if menu is closed + LaunchedEffect(showQueryOptions) { + if (showQueryOptions) { + val initEntry = MENU_OPTIONS_SEARCH.entries.first() + selectedSearchQuery[initEntry.key] = initEntry.value.first() + } else { + selectedSearchQuery.clear() + } + } + if (showQueryOptions) { + MenuTextInput( + menuLabels = MENU_LABELS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + MenuDropDown( + menuOptions = MENU_OPTIONS_SEARCH, + ) { label, selected -> + selectedSearchQuery[label] = selected + } + } + + // form final deeplink url + val arguments = when (selectedPath.value) { + PATH_INCLUDE -> "/${selectedFilter}" + PATH_SEARCH -> { + buildString { + selectedSearchQuery.forEach { entry -> + if (entry.value.isNotEmpty()) { + val prefix = if (isEmpty()) "?" else "&" + append("$prefix${entry.key}=${entry.value}") + } + } + } + } + + else -> "" + } + val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments" + TextContent("Final url:\n$finalUrl") + // deeplink to target + PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { + val intent = Intent( + this@UriWithArgumentsDeepLinkActivity, + MainActivity::class.java + ) + // start activity with the url + intent.data = finalUrl.toUri() + startActivity(intent) + }) + } + } + } +} + +private const val KEY_PATH = "path" +private val MENU_OPTIONS_PATH = mapOf( + KEY_PATH to listOf( + STRING_LITERAL_HOME, + PATH_INCLUDE, + PATH_SEARCH, + ), +) + +private val MENU_OPTIONS_FILTER = mapOf( + UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED, UsersKey.FILTER_OPTION_ALL), +) + +private val MENU_OPTIONS_SEARCH = mapOf( + SearchKey::firstName.name to listOf( + EMPTY, + FIRST_NAME_JOHN, + FIRST_NAME_TOM, + FIRST_NAME_MARY, + FIRST_NAME_JULIE + ), + SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US) +) + +private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) + + + +``` + +``` +package com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui + +import com.example.nav3recipes.deeplink.handlerequests.uriarguments.SearchKey + +/** + * String resources + */ +internal const val STRING_LITERAL_FILTER = "filter" +internal const val STRING_LITERAL_HOME = "home" +internal const val STRING_LITERAL_USERS = "users" +internal const val STRING_LITERAL_SEARCH = "search" +internal const val STRING_LITERAL_INCLUDE = "include" +internal const val PATH_BASE = "https://www.nav3recipes.com" +internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE" +internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH" +internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME" + +internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}" +internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + + "?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" + + "&${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 6ef8efe..543c21f 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,4 +104,6 @@ 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 new file mode 100644 index 0000000..90c4317 --- /dev/null +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md @@ -0,0 +1,187 @@ +# LocalLifecycleOwner Recipe + +This recipe demonstrates how to use `LifecycleResumeEffect` in Navigation 3 entries to pause and resume work based on the entry's lifecycle state. + +## How it works + +In Navigation 3, by default each `NavEntry` is provided its own `LifecycleOwner` via `LocalLifecycleOwner.current`. This means that any lifecycle-aware components inside the entry is automatically scoped to the `NavEntry`. + +### `LifecycleResumeEffect` with Dialog Scenes + +1. **RouteA (Screen)**: + + - Uses `LifecycleResumeEffect(Unit)` scoped to the `NavEntry`'s `LocalLifecycleOwner.current` to advance the`LinearProgressIndicator` while in the `RESUMED` state. + - Automatically resets `progressValue` back to `0f` whenever it hits `1f`. +2. **RouteB (Dialog)**: + + - Configured as a dialog using `DialogSceneStrategy.dialog()`. + - When the user opens the RouteB dialog, RouteA remains visible behind the dialog in the `STARTED` state (leaving `RESUMED`). + - `LifecycleResumeEffect` calls `onPauseOrDispose`, pausing the progress indicator. + - When the dialog is dismissed, RouteA returns to `RESUMED`, and `LifecycleResumeEffect` resumes the progress indicator automatically. + +[![](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/lifecycleowner) + +``` +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.nav3recipes.lifecycleowner + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.ui.NavDisplay +import com.example.nav3recipes.content.ContentGreen +import com.example.nav3recipes.ui.setEdgeToEdgeConfig +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.milliseconds + +@Serializable +private data object RouteA : NavKey + +@Serializable +private data object RouteB : NavKey + +class LifecycleOwnerActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + setEdgeToEdgeConfig() + super.onCreate(savedInstanceState) + setContent { + val backStack = rememberNavBackStack(RouteA) + val dialogStrategy = remember { DialogSceneStrategy() } + + NavDisplay( + backStack = backStack, + onBack = backStack::removeLastOrNull, + sceneStrategies = listOf(dialogStrategy), + entryProvider = entryProvider { + entry { + LogLifecycleEffect("ScreenA") + ContentGreen("Screen A") { + var progressValue by remember { mutableFloatStateOf(0f) } + val coroutineScope = rememberCoroutineScope() + + val animatedProgress by animateFloatAsState( + targetValue = progressValue, + animationSpec = if (progressValue == 0f) snap() else tween(durationMillis = 100, easing = LinearEasing), + label = "ProgressAnimation" + ) + + // LifecycleResumeEffect runs only while ScreenA is in the RESUMED state. + // When the dialog opens, ScreenA transitions to PAUSED, which calls onPauseOrDispose. + LifecycleResumeEffect(Unit) { + val job = coroutineScope.launch { + while (true) { + delay(100.milliseconds) + progressValue += 0.01f + if (progressValue >= 1f) { + progressValue = 0f + } + } + } + onPauseOrDispose { + job.cancel() + } + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + LinearProgressIndicator(progress = { animatedProgress }) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = dropUnlessResumed { backStack.add(RouteB) }) { + Text("Open Dialog") + } + } + } + } + entry(metadata = DialogSceneStrategy.dialog()) { + LogLifecycleEffect("Dialog") + AlertDialog( + onDismissRequest = backStack::removeLastOrNull, + title = { Text("Dialog") }, + text = { + Text("Screen A is no longer in resumed state, pausing the progress indicator.") + }, + confirmButton = { + Button(onClick = dropUnlessResumed { backStack.removeLastOrNull() }) { + Text("Dismiss Dialog") + } + } + ) + } + } + ) + } + } +} + +@Composable +private fun LogLifecycleEffect(screenName: String) { + LifecycleEventEffect(Lifecycle.Event.ON_CREATE) { + Log.d("LifecycleOwner", "$screenName: ON_CREATE") + } + LifecycleEventEffect(Lifecycle.Event.ON_START) { + Log.d("LifecycleOwner", "$screenName: ON_START") + } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + Log.d("LifecycleOwner", "$screenName: ON_RESUME") + } + LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) { + Log.d("LifecycleOwner", "$screenName: ON_PAUSE") + } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + Log.d("LifecycleOwner", "$screenName: ON_STOP") + } + // 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 fbaae79..032a997 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,4 +138,6 @@ 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 58a6a7c..d20f52e 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,4 +142,6 @@ 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 fcc94a2..e4550b8 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,6 +75,8 @@ private fun ProfileScreen() { ) } } + + ``` ``` @@ -118,6 +120,8 @@ class HiltModularActivity : ComponentActivity() { } } } + + ``` ``` @@ -236,6 +240,8 @@ private fun ConversationDetailScreen( } } } + + ``` ``` @@ -261,6 +267,7 @@ class Navigator(startDestination: Any) { backStack.removeLastOrNull() } } + ``` ``` @@ -280,4 +287,6 @@ 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 d1e7ad6..6e59bdc 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,6 +66,8 @@ private fun ProfileScreen() { ) } } + + ``` ``` @@ -181,6 +183,8 @@ private fun ConversationDetailScreen( } } } + + ``` ``` @@ -200,6 +204,7 @@ class Navigator(startDestination: Any) { backStack.removeLastOrNull() } } + ``` ``` @@ -217,6 +222,7 @@ val appModule = module { } } } + ``` ``` @@ -284,4 +290,6 @@ 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 81cd794..8831896 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 @@ -2,7 +2,7 @@ This recipe demonstrates how to create multiple back stacks. -The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. The content for the sub routes is a counter that can be used to verify state retention through configuration changes and process death. +The app has three top level routes: `RouteA`, `RouteB` and `RouteC`. These routes have sub routes `RouteA1`, `RouteB1` and `RouteC1` respectively. `RouteA1` contains a 100-item scrollable list, while `RouteB1` and `RouteC1` contain counters used to verify state retention through configuration changes and process death. The app's navigation state is held in the `NavigationState` class. The state itself is created using `rememberNavigationState`. @@ -14,6 +14,7 @@ Key behaviors: - This app follows the "exit through home" pattern where the user always exits through the starting back stack. This means that `RouteA`'s entries are *always* in the list of entries. - Navigating to a top level route that is not the starting route *replaces* the other entries. For example, navigating A-\>B-\>C would result in entries for A+C, B's entries are removed. +- When a top-level route is reselected, for example if the user is on `RouteA` and taps `RouteA` on the navigation bar again, the `NavigationBar` signals the reselected key to `Navigator`, which emits it to a `Flow`. `RouteA1` observes this flow and, when it receives `RouteA`, resets its list scroll position to index 0. Important implementation details: @@ -41,11 +42,16 @@ Important implementation details: package com.example.nav3recipes.multiplestacks import androidx.navigation3.runtime.NavKey +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow /** * Handles navigation events (forward and back) by updating the navigation state. */ class Navigator(val state: NavigationState){ + private val _reselectEvents = MutableSharedFlow(extraBufferCapacity = 1) + val reselectEvents = _reselectEvents.asSharedFlow() + fun navigate(route: NavKey){ if (route in state.backStacks.keys){ // This is a top level route, just switch to it @@ -55,6 +61,10 @@ class Navigator(val state: NavigationState){ } } + fun onReselect(route: NavKey) { + _reselectEvents.tryEmit(route) + } + fun goBack(){ val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") @@ -68,6 +78,8 @@ class Navigator(val state: NavigationState){ } } } + + ``` ``` @@ -204,6 +216,8 @@ class NavigationState( listOf(startRoute, topLevelRoute) } } + + ``` ``` @@ -225,10 +239,11 @@ class NavigationState( package com.example.nav3recipes.multiplestacks -import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Camera import androidx.compose.material.icons.filled.Face @@ -239,7 +254,9 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay @@ -277,7 +294,6 @@ data class NavBarItem( ) class MultipleStacksActivity : ComponentActivity() { - @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) @@ -290,18 +306,26 @@ class MultipleStacksActivity : ComponentActivity() { val navigator = remember { Navigator(navigationState) } val entryProvider = entryProvider { - featureASection(onSubRouteClick = { navigator.navigate(RouteA1) }) + featureASection( + reselectEvents = navigator.reselectEvents, + onSubRouteClick = { navigator.navigate(RouteA1) } + ) featureBSection(onSubRouteClick = { navigator.navigate(RouteB1) }) featureCSection(onSubRouteClick = { navigator.navigate(RouteC1) }) } - Scaffold(bottomBar = { + Scaffold(contentWindowInsets = WindowInsets(0.dp), bottomBar = { NavigationBar { TOP_LEVEL_ROUTES.forEach { (key, value) -> val isSelected = key == navigationState.topLevelRoute NavigationBarItem( selected = isSelected, - onClick = { navigator.navigate(key) }, + onClick = { + navigator.navigate(key) + if (isSelected) { + navigator.onReselect(key) + } + }, icon = { Icon( imageVector = value.icon, @@ -312,15 +336,18 @@ class MultipleStacksActivity : ComponentActivity() { ) } } - }) { + }) { innerPadding -> NavDisplay( entries = navigationState.toDecoratedEntries(entryProvider), - onBack = { navigator.goBack() } + onBack = { navigator.goBack() }, + modifier = Modifier.padding(innerPadding) ) } } } } + + ``` ``` @@ -343,13 +370,17 @@ class MultipleStacksActivity : ComponentActivity() { package com.example.nav3recipes.multiplestacks import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey @@ -359,27 +390,34 @@ import com.example.nav3recipes.content.ContentOrange import com.example.nav3recipes.content.ContentPink import com.example.nav3recipes.content.ContentPurple import com.example.nav3recipes.content.ContentRed +import kotlinx.coroutines.flow.Flow fun EntryProviderScope.featureASection( + reselectEvents: Flow, onSubRouteClick: () -> Unit, ) { entry { ContentRed("Route A") { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { - Text("Go to A1") - } + Button(onClick = dropUnlessResumed(block = onSubRouteClick)) { + Text("Go to A1") } } } entry { - ContentPink("Route A1") { - var count by rememberSaveable { - mutableIntStateOf(0) + val scrollState = rememberLazyListState() + LaunchedEffect(reselectEvents) { + reselectEvents.collect { route -> + if (route == RouteA) { + scrollState.scrollToItem(0) + } } + } - Button(onClick = { count++ }) { - Text("Value: $count") + ContentPink("Route A1") { + LazyColumn(state = scrollState) { + items(100) { index -> + Text("Route A item ${index + 1}", fontSize = 24.sp) + } } } } @@ -433,4 +471,6 @@ 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 2640a22..b00cd74 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,6 +133,8 @@ class RouteBViewModel @AssistedInject constructor( fun create(navKey: RouteB): RouteBViewModel } } + + ``` # Passing Arguments to ViewModels (Basic) @@ -257,6 +259,8 @@ class RouteBViewModel( } } } + + ``` # Passing Arguments to ViewModels (Koin) @@ -368,4 +372,5 @@ 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 ea7d27d..a8c510e 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,6 +41,7 @@ import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { var person by mutableStateOf(null) } + ``` ``` @@ -70,6 +71,7 @@ data object Home : NavKey @Serializable class PersonDetailsForm : NavKey + ``` ``` @@ -95,6 +97,7 @@ import kotlinx.serialization.Serializable @Serializable data class Person(val name: String, val favoriteColor: String) + ``` ``` @@ -180,6 +183,7 @@ fun PersonDetailsScreen( } } } + ``` ``` @@ -270,4 +274,6 @@ 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 a73e148..4380a9d 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,6 +41,7 @@ import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { var person by mutableStateOf(null) } + ``` ``` @@ -70,6 +71,7 @@ data object Home : NavKey @Serializable class PersonDetailsForm : NavKey + ``` ``` @@ -95,6 +97,7 @@ import kotlinx.serialization.Serializable @Serializable data class Person(val name: String, val favoriteColor: String) + ``` ``` @@ -180,6 +183,7 @@ fun PersonDetailsScreen( } } } + ``` ``` @@ -263,4 +267,6 @@ 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 32063b3..4a2947b 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,6 +168,8 @@ class ListDetailSceneStrategy(val windowSizeClass: WindowSizeClass) : S ) } } + + ``` ``` @@ -287,6 +289,7 @@ private fun NavBackStack.addDetail(detailRoute: ConversationDetail) { removeIf { it is ConversationDetail } add(detailRoute) } + ``` ``` @@ -432,4 +435,5 @@ 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 fed0bf9..513494a 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,6 +123,8 @@ class TwoPaneSceneStrategy(val windowSizeClass: WindowSizeClass) : Scen } + + ``` ``` @@ -241,4 +243,6 @@ 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 c570000..8959d00 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,11 +1,8 @@ -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**: @@ -43,8 +40,7 @@ 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:** @@ -83,8 +79,7 @@ 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:** @@ -98,10 +93,9 @@ 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)) @@ -124,6 +118,5 @@ a custom `NavType`. - **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 2812549..f3ab821 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-07-18' + last-updated: '2026-08-06' keywords: - R8 - proguard @@ -20,33 +20,39 @@ metadata: - Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties`. - Use [references/CONFIGURATION.md](references/CONFIGURATION.md) to identify missing optimizations. -- **AGP** : If \< 9.0, suggest migration to 9.0 for [build time improvement - performance](references/android/topic/performance/app-optimization/enable-app-optimization.md) +- **AGP** : If version is lower than 9.0, suggest migration to 9.0 for [build-time performance improvement](references/android/topic/performance/app-optimization/enable-app-optimization.md). - **Full Mode** : Verify `android.enableR8.fullMode=false` is removed from gradle.properties. ## Step 2. Analysis path selection -- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and - `libs.versions.toml` to get the R8 version +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and `libs.versions.toml` to get the AGP and R8 versions. -- **If R8 \>= 9.3.7-dev** : Proceed to **Path A (Quantitative)**. +- **If AGP \>= 9.3.0** : Proceed to **Path A (Standalone Task)**. -- **If R8 \< 9.3.7-dev** : Proceed to **Path B (Heuristic)**. +- **If AGP \< 9.3.0 and R8 \>= 9.3.7-dev** : Proceed to **Path B (Quantitative)**. -### Path A: Quantitative data generation (R8 \>= 9.3.7-dev) +- If none of the conditions are met, proceed to **Path C (Heuristic)**. -- **Check requirements** : Python and `protobuf` package are mandatory. -- **Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to json and analyze the result. -- **Report** : Rely entirely on the generated file `analysis.txt` for scores and rule impact metrics. Proceed to Step 3. +### Path A: Standalone Gradle task (AGP \>= 9.3.0) -### Path B: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) +- **Step 1: Run standalone task** : Run `./gradlew :app:analyzeReleaseR8Config` to evaluate the R8 configuration. You MUST wait for this command to finish before proceeding. +- **Step 2: Convert to JSON** : The report is generated at `app/build/reports/r8/r8-config-analyzer-release.pb`. You MUST explicitly run the conversion script by executing: `python3 .agents/skills/r8-analyzer/scripts/convert_pb_to_json.py`. Wait for this command to finish. +- **Step 3: Analyze** : You MUST explicitly run the analysis script by executing: `python3 .agents/skills/r8-analyzer/scripts/analyze.py`. This outputs `tmp/keepradius/analysis_result.txt`. Wait for this command to finish. + +### Path B: Quantitative data generation (R8 \>= 9.3.7-dev and AGP \< 9.3.0) + +- **Step 1: Check requirements** : Python and `protobuf` package are mandatory. +- **Step 2: Generate and analyze** : You MUST run the shell commands described in [references/CONFIGURATION-ANALYZER.md](references/CONFIGURATION-ANALYZER.md) to generate the proto file using R8 configuration analyzer, convert it to JSON and analyze the result. +- **Step 3: Analyze** : You MUST ensure the analysis produces `tmp/keepradius/analysis_result.txt` for scores and rule impact metrics. + +### Path C: Heuristic evaluation and recommendation (R8 \< 9.3.7-dev) *(Use ONLY if quantitative data generation is not possible)* -- **Manual evaluation** : Inspect `proguard-rules.pro`. -- **Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. -- **Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). -- **Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. +- **Step 1: Manual evaluation** : Inspect `proguard-rules.pro`. +- **Step 2: Library check** : Compare rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). Suggest **Remove** for bundled rules. +- **Step 3: Custom rule check** : Use [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md) to prioritize and evaluate. Suggest **Refine** for broad rules (for example, package-wide). +- **Step 4: Validation** : Suggest Macrobenchmark tests using [UI Automator](references/android/training/testing/other-components/ui-automator.md) for any proposed changes. Proceed to Step 3. ## Step 3. Report generation diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md index 4039dcf..84cf3d1 100644 --- a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md +++ b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -6,17 +6,13 @@ 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 @@ -29,19 +25,14 @@ bash commands: ### 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 @@ -93,8 +84,7 @@ in the same directory. ### 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 @@ -150,8 +140,7 @@ 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 @@ -198,27 +187,22 @@ following script. 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 ae04947..81263a9 100644 --- a/performance/r8-analyzer/references/CONFIGURATION.md +++ b/performance/r8-analyzer/references/CONFIGURATION.md @@ -1,12 +1,8 @@ -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`):** @@ -38,7 +34,6 @@ type for release and performance testing. It MUST use the optimized default file - **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 2e07317..c01a9be 100644 --- a/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +++ b/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md @@ -1,53 +1,38 @@ -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 { *; } @@ -60,15 +45,13 @@ 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 @@ -76,8 +59,7 @@ retention logic in the rule itself ## 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 0021890..0bfe0e2 100644 --- a/performance/r8-analyzer/references/REDUNDANT-RULES.md +++ b/performance/r8-analyzer/references/REDUNDANT-RULES.md @@ -1,36 +1,20 @@ -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. @@ -38,18 +22,11 @@ referenced in XML layout files. ## 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. *** ** * ** *** @@ -57,18 +34,14 @@ within these dependencies. ### 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 @@ -77,8 +50,7 @@ globally prevents the removal of unused adapter implementations. ### 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 @@ -86,8 +58,7 @@ necessary `TypeToken` preservation. ### 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.** { *; } @@ -101,22 +72,17 @@ code within the library. **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.** { *; } @@ -124,70 +90,54 @@ entire library. ### 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:** @@ -201,15 +151,12 @@ android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`. ## 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. @@ -217,6 +164,4 @@ layers. ## 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 8f23679..dd1cca5 100644 --- a/performance/r8-analyzer/references/REFLECTION-GUIDE.md +++ b/performance/r8-analyzer/references/REFLECTION-GUIDE.md @@ -1,139 +1,81 @@ -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 629c777..3527b6d 100644 --- a/performance/r8-analyzer/references/REPORT_FORMAT.md +++ b/performance/r8-analyzer/references/REPORT_FORMAT.md @@ -11,8 +11,7 @@ - \[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 @@ -20,8 +19,7 @@ mention in this section - **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 @@ -42,10 +40,4 @@ 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 a80f19d..95c82e3 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,7 +1,18 @@ -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: +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/performance/r8-analyzer) + +### R8 Analyzer + +Use the R8 Analyzer [Android skill](https://developer.android.com/tools/agents/android-skills) to analyze build files and identify redundant, broad, or subsumed [keep rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview). To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill r8-analyzer + +
+ +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 @@ -9,49 +20,27 @@ performance, and more. To your users, this means: - 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] -> **Important:** We released an agent skill that you can use to improve your app performance with R8. Try out the skill from the [Android skills repository](https://github.com/android/skills). +> **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 @@ -61,8 +50,7 @@ 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 @@ -90,10 +78,7 @@ 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) @@ -141,9 +126,7 @@ settings. ## 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) @@ -158,70 +141,48 @@ 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. +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.` -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). | 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 2b5b253..75d0d08 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,34 +1,26 @@ -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 @@ -50,9 +42,7 @@ 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 @@ -62,13 +52,9 @@ and type-safe environment for your test operations. ### 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() @@ -81,15 +67,12 @@ state, and content description. 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 } @@ -97,9 +80,7 @@ state, and content description. 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. @@ -108,8 +89,7 @@ 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() @@ -119,8 +99,7 @@ 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() @@ -133,8 +112,7 @@ fields. ## 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 @@ -155,9 +133,7 @@ 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 @@ -180,24 +156,19 @@ uses the internal watcher mechanism but offers more flexibility. 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() @@ -210,26 +181,11 @@ before interacting with it. ## 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 @@ -237,9 +193,7 @@ 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() @@ -250,9 +204,7 @@ such as Picture-in-Picture (PiP) mode or split-screen layouts. ### 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 @@ -270,14 +222,11 @@ regression testing and debugging. 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") @@ -299,8 +248,7 @@ debugging. ## 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 0493cd7..9479934 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-07-18' + last-updated: '2026-08-06' keywords: - android - engage @@ -15,9 +15,7 @@ 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 @@ -54,8 +52,7 @@ 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:** @@ -72,9 +69,7 @@ Follow these steps to assist the developer: - 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: +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` @@ -92,8 +87,7 @@ 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:** @@ -102,7 +96,7 @@ Follow these steps to assist the developer: - [Listen Vertical](references/android/guide/playcore/engage/listen.md) - [Read Vertical](references/android/guide/playcore/engage/read.md) - [Shopping Vertical](references/android/guide/playcore/engage/shopping.md) - - [Social Vertical](https://developer.android.com/guide/playcore/engage/social) + - [Social Vertical](references/android/guide/playcore/engage/social.md) - [Travel Vertical](references/android/guide/playcore/engage/travel.md) - [Health and Fitness Vertical](references/android/guide/playcore/engage/healthandfitness.md) - [Other Verticals](references/android/guide/playcore/engage/otherverticals.md) 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 3ed6401..e0d1f4a 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,47 +2,29 @@ #### 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 @@ -77,27 +59,19 @@ 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 @@ -105,33 +79,24 @@ calculate impact. #### 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? @@ -145,8 +110,7 @@ older "Cross Device Play Next" system is being phased out. #### 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? @@ -155,85 +119,53 @@ 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): @@ -258,29 +190,23 @@ 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. @@ -293,49 +219,29 @@ Here's why: #### 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 @@ -347,25 +253,19 @@ 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 @@ -384,15 +284,9 @@ cases can be performed on other surfaces as well. 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. @@ -404,8 +298,7 @@ cases can be performed on other surfaces as well. #### 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? @@ -416,8 +309,7 @@ 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. @@ -425,15 +317,12 @@ the key scenarios: 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 dea9db9..72aa976 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,59 +1,32 @@ -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. @@ -73,11 +46,9 @@ 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 | |---|---|---| @@ -89,8 +60,7 @@ cluster types: ### 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` @@ -103,9 +73,7 @@ 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.
@@ -133,8 +101,7 @@ publish. #### `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.
@@ -160,8 +127,7 @@ want to publish, such as a restaurant or a grocery store. #### `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.
@@ -264,10 +230,7 @@ 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. @@ -291,16 +254,29 @@ 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -361,26 +337,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -461,14 +422,12 @@ 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` @@ -497,14 +456,12 @@ 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` @@ -533,14 +490,12 @@ 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` @@ -569,14 +524,12 @@ 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` @@ -605,20 +558,16 @@ 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 - @@ -671,21 +620,16 @@ 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. @@ -725,11 +669,7 @@ 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 @@ -760,9 +700,7 @@ 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. @@ -782,9 +720,7 @@ 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. @@ -804,9 +740,7 @@ 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. @@ -826,9 +760,7 @@ 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. @@ -848,9 +780,7 @@ 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. @@ -870,9 +800,7 @@ 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. @@ -902,15 +830,11 @@ 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() @@ -932,8 +856,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -947,23 +870,13 @@ error code. ### 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 @@ -1069,10 +982,7 @@ 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. @@ -1109,18 +1018,15 @@ service: ## 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 3d6a779..5cd2518 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,83 +1,43 @@ -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 @@ -94,11 +54,9 @@ 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 | |---|---|---|---| @@ -108,8 +66,7 @@ cluster types: ### 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` @@ -255,8 +212,7 @@ 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* @@ -273,8 +229,7 @@ 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` @@ -294,33 +249,25 @@ categories. This maps the content with some of the predefined categories namely: - `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. @@ -340,16 +287,29 @@ 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -407,26 +367,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -500,14 +445,12 @@ 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` @@ -538,14 +481,12 @@ 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` @@ -576,20 +517,16 @@ 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 - @@ -642,20 +579,16 @@ 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. @@ -695,11 +628,7 @@ 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 @@ -730,9 +659,7 @@ 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. @@ -752,9 +679,7 @@ 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. @@ -774,9 +699,7 @@ 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. @@ -796,9 +719,7 @@ 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. @@ -828,15 +749,11 @@ 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 @@ -880,8 +797,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -895,23 +811,13 @@ error code. ### 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,10 +888,7 @@ 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. @@ -1014,19 +916,15 @@ service: ## 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 1bdb7c3..1285698 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,47 +1,26 @@ -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). @@ -58,11 +37,9 @@ 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 | |---|---|---| @@ -72,8 +49,7 @@ cluster types: ### 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` @@ -89,8 +65,7 @@ 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 | |---|---|---| @@ -126,8 +101,7 @@ 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 | |---|---|---| @@ -146,8 +120,7 @@ Coldplay). #### `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 | |---|---|---| @@ -167,8 +140,7 @@ The `MusicVideoEntity` object represents a music video (for example, #### `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 | |---|---|---| @@ -186,8 +158,7 @@ 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 | |---|---|---| @@ -206,8 +177,7 @@ American Life*). #### `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 | |---|---|---| @@ -232,8 +202,7 @@ The `PodcastEpisodeEntity` object represents a podcast series (for example, #### `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 | |---|---|---| @@ -249,8 +218,7 @@ example, 98.1 The Breeze). #### `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 | |---|---|---| @@ -338,13 +306,9 @@ 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` @@ -360,16 +324,29 @@ APIs are available 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -427,25 +404,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -517,14 +480,12 @@ 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` @@ -553,14 +514,12 @@ 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` @@ -591,20 +550,16 @@ 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 - @@ -657,21 +612,16 @@ 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. @@ -711,11 +661,7 @@ 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 @@ -746,9 +692,7 @@ 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. @@ -768,9 +712,7 @@ 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. @@ -790,9 +732,7 @@ 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. @@ -812,9 +752,7 @@ 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. @@ -844,15 +782,11 @@ 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() @@ -874,8 +808,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -889,23 +822,13 @@ error code. ### 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 @@ -976,10 +899,7 @@ 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. @@ -1008,20 +927,15 @@ service: ## 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 f1e7de9..67463e0 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,86 +1,43 @@ -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 @@ -90,34 +47,18 @@ 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 @@ -134,11 +75,9 @@ 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 | |---|---|---|---| @@ -148,8 +87,7 @@ cluster types: ### 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` @@ -257,8 +195,7 @@ 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 | |---|---|---|---| @@ -493,8 +430,7 @@ 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* @@ -511,8 +447,7 @@ 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` @@ -532,33 +467,25 @@ categories. This maps the content with some of the predefined categories namely: - `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. @@ -578,16 +505,29 @@ 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -654,26 +594,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -747,14 +672,12 @@ 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` @@ -785,14 +708,12 @@ 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` @@ -823,20 +744,16 @@ 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 - @@ -889,20 +806,16 @@ 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. @@ -942,11 +855,7 @@ 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 @@ -977,9 +886,7 @@ 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. @@ -999,9 +906,7 @@ 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. @@ -1021,9 +926,7 @@ 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. @@ -1043,9 +946,7 @@ 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. @@ -1075,15 +976,11 @@ 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 @@ -1127,8 +1024,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -1142,23 +1038,13 @@ error code. ### 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 @@ -1228,10 +1114,7 @@ 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. @@ -1260,19 +1142,15 @@ service: ## 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 ccd4514..bb51a5f 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,49 +1,29 @@ > [!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). @@ -60,11 +40,9 @@ 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 | |---|---|---| @@ -74,8 +52,7 @@ cluster types: ### 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` @@ -85,8 +62,7 @@ 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 | |---|---|---| @@ -110,8 +86,7 @@ The `EbookEntity` object represents an ebook (for example, the ebook of #### `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 | |---|---|---| @@ -136,8 +111,7 @@ of *Becoming* by Michelle Obama). #### `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 | |---|---|---| @@ -200,13 +174,9 @@ 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` @@ -222,16 +192,29 @@ APIs are available 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -288,26 +271,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -379,14 +347,12 @@ 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` @@ -415,14 +381,12 @@ 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` @@ -453,20 +417,16 @@ 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 - @@ -519,21 +479,16 @@ 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. @@ -573,11 +528,7 @@ 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 @@ -608,9 +559,7 @@ 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. @@ -630,9 +579,7 @@ 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. @@ -652,9 +599,7 @@ 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. @@ -674,9 +619,7 @@ 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. @@ -706,15 +649,11 @@ 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() @@ -736,8 +675,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -751,23 +689,13 @@ error code. ### 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 @@ -838,10 +766,7 @@ 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. @@ -870,18 +794,15 @@ service: ## 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 4e24895..efc4644 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,78 +1,40 @@ -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 @@ -92,16 +54,13 @@ 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 | |---|---|---| @@ -114,8 +73,7 @@ cluster types: ### 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` @@ -127,8 +85,7 @@ 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` @@ -231,10 +188,7 @@ 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. @@ -260,16 +214,29 @@ 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -331,26 +298,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -431,14 +383,12 @@ 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` @@ -467,20 +417,16 @@ 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. @@ -505,14 +451,12 @@ merchant name in the title when using this API. .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` @@ -541,14 +485,12 @@ 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` @@ -577,14 +519,12 @@ 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` @@ -613,20 +553,16 @@ 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 - @@ -679,21 +615,16 @@ 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. @@ -733,11 +664,7 @@ 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 @@ -768,9 +695,7 @@ 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. @@ -790,9 +715,7 @@ 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. @@ -812,9 +735,7 @@ 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. @@ -834,9 +755,7 @@ 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. @@ -856,9 +775,7 @@ 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. @@ -878,9 +795,7 @@ 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. @@ -900,9 +815,7 @@ 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. @@ -932,15 +845,11 @@ 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 @@ -984,8 +893,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -999,23 +907,13 @@ error code. ### 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 @@ -1138,10 +1036,7 @@ 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. @@ -1182,20 +1076,15 @@ service: ## 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 new file mode 100644 index 0000000..32e630b --- /dev/null +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md @@ -0,0 +1,649 @@ +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. + +## Integration detail + +The following section captures the integration detail. + +### Terminology + +***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. + +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. + +- Post + + - Image in portrait mode and Timestamp, or + - Image in portrait mode + text content and Timestamp +- Profile + + - Avatar, Name or Handle, Additional image +- Interactions + + - Count and label only, or + - Count and visual (icon) + +**SocialPostEntity** contains profile, post and interaction related metadata. + +- Profile + + - Avatar, Name or Handle, additional text, additional image +- Post + + - Text and Timestamp, or + - Rich media (image or rich URL) and Timestamp, or + - Text and rich media (image or rich URL) and Timestamp, or + - Video preview (thumbnail and duration) and Timestamp +- Interactions + + - Count \& label only, or + - Count \& visual (icon) + +### Pre-work + +Minimum API level: 19 + +Add the `com.google.android.engage:engage-core` library to your app: + + dependencies { + // Make sure you also include that repository in your project's build.gradle file. + implementation 'com.google.android.engage:engage-core:1.6.0' + } + +### Summary + +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: + +| Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | +|---|---|---|---| +| Recommendation Cluster(s) | At most 7 | At least 1 (`PortraitMediaEntity`, or `SocialPostEntity`) | At most 50 (`PortraitMediaEntity`, or `SocialPostEntity`) | + +### 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: + +1. `PortraitMediaEntity` +2. `SocialPostEntity` + +The charts below outline available attributes and requirements for each type. + +#### `PortraitMediaEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** for all surfaces other than Google TV | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** |||| +| Image(s) | Required | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| Is video content | Optional | Is the post a video? | boolean | +| Video duration | Optional | The duration of the video in milliseconds. | Long | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel" | String(recommended max 25 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge. for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Optional | Indicate the number of interactions, for example - "3.7 M.". **Note:** If both Count and Count Value are provided, Count will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Optional | Indicate the number of interactions with an optional label, for example - "3.7 M Likes.". **Note:** If both CountWithOptionalLabel and Count Value are provided, one of them will be used. **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Count Value | Optional | The number of interactions as a value. **Note:** Provide Count Value instead of Count if your app doesn't handle logic on how a large number should be optimized for different display sizes. If both Count and Count Value are provided, Count is used. | Long | +| Label | Optional | Indicate what the interaction label is for. For example - "Likes". | String | +| Visual | Optional | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Note:** Must be Square 1:1 image | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### `SocialPostEntity` + +| Attribute | Requirement | Description | Format | +|---|---|---|---| +| Action URI | **Required** | Deep Link to the entity in the provider app. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | URI | +| PlatformSpecificPlayback URIs | **Required** for Google TV surface | Deep Link to the entity in the provider app for platforms like Google TV and Mobile. | List of PlatformSpecificPlayback objects | +| Recommendation Reason | Optional | The justification for recommending the content to the user. | RecommendationReason object | +| Comments Summary | Optional | Summary of comments for the post. | String | +| **Post related metadata (Required)** At least one of TextContent, Image or WebContent is required |||| +| Image(s) | Optional | Image(s) should be in **portrait aspect ratio.** The UI may show only 1 image when multiple images are provided. However, the UI may provide visual indication that there are more images in the app. *If the post is a video, the provider should provide a thumbnail of the video to be shown as an image.* | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Text content | Optional | The main text of a post, update, etc. | String (recommended max 140 chars) | +| **Video Content (Optional)** |||| +| Duration | Required | The duration of the video in milliseconds. | Long | +| Image | Required | Preview image of the video content. | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Link Preview (Optional)** |||| +| Link Preview - Title | Required | Text to indicate the title of the web page content | String | +| Link Preview - Hostname | Required | Text to indicate the web page owner, eg "INSIDER" | String | +| Link Preview - Image | Optional | Hero image for the web content | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Timestamp | Optional | Time when the post was published. | Epoch timestamp in milliseconds | +| **Profile related metadata (Optional)** |||| +| Name | Required | Profile name or id or handle, eg "John Doe", "@TeamPixel." | String(recommended max 25 chars) | +| Additional Text | Optional | Could be used as profile id or handle or additional metadata For example "@John-Doe", "5M followers", "You might like", "Trending", "5 new posts" | String(recommended max 40 chars) | +| Avatar | Required | Profile picture or avatar image of the user. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| Additional Image | Optional | Profile badge, for example - verified badge **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **Interactions related metadata (Optional)** |||| +| Count | Required | Indicate the number of interactions, for example - "3.7 M." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| CountWithOptionalLabel | Required | Indicate the number of interactions with an optional label, for example - "3.7 M Likes." **Note:** Partners should use either **Count** or **CountWithOptionalLabel**. | String | +| Label | Optional If not provided, **Visual** must be provided. | Indicate what the interaction is for. For example - "Likes." | String (recommended max 20 chars for count + label combined) | +| Visual | Optional If not provided, **Label** must be provided. | Indicate what the interaction is for. For example - Image showing Likes icon, emoji. Can provide more than 1 image, though not all may not be shown on all form factors. **Square 1:1 image** | See [Image Specifications](https://developer.android.com/guide/playcore/engage/social#image-specs) for guidance. | +| **DisplayTimeWindow (Optional) - Set a time window for a content to be shown on the surface** |||| +| Start Timestamp | Optional | The epoch timestamp after which the content should be shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | +| End Timestamp | Optional | The epoch timestamp after which the content is no longer shown on the surface. If not set, content is eligible to be shown on the surface. | Epoch timestamp in milliseconds | + +#### Image specifications + +The images are required to be hosted on public CDNs so that Google can access them. + +*File formats* + +PNG, JPG, static GIF, WebP + +*Maximum file size* + +5120 KB + +*Additional recommendations* + +- **Image safe area:** Put your important content in the center 80% of the image. +- Use a transparent background so that the image can be properly displayed in Dark and Light theme settings. + +### 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) + +`AppEngageSocialClient` is responsible for publishing social clusters. + +There are following APIs to publish clusters in the client: + +- `isServiceAvailable` +- `publishRecommendationClusters` +- `publishUserAccountManagementRequest` +- `updatePublishStatus` +- `deleteRecommendationsClusters` +- `deleteUserManagementCluster` +- `deleteClusters` + +#### `isServiceAvailable` + +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 + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | +| Featured Cluster | `TYPE_FEATURED` | 2 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | +| User Management Cluster | `TYPE_ENGAGEMENT` | 8 | +| Subscription Cluster | `TYPE_SUBSCRIPTION` | 12 | + +### Kotlin + + 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 { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + ServiceAvailabilityRequest request = + new ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build(); + + client.isServiceAvailable(request).addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Map availabilityMap = task.getResult(); + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_CONTINUATION))) { + // Proceed with publishing continuation content + } + if (Boolean.TRUE.equals(availabilityMap.get(ClusterType.TYPE_RECOMMENDATION))) { + // Proceed with publishing recommendation content + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +###### 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. + +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) + +### Kotlin + + client.isServiceAvailable.addOnCompleteListener { task -> + if (task.isSuccessful) { + // Handle IPC call success + if(task.result) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + } + +### Java + + client.isServiceAvailable().addOnCompleteListener(task - > { + if (task.isSuccessful()) { + // Handle success + if(task.getResult()) { + // Service is available on the device, proceed with content publish + // calls. + } else { + // Service is not available, no further action is needed. + } + } else { + // The IPC call itself fails, proceed with error handling logic here, + // such as retry. + } + }); + +> [!NOTE] +> **Note:** We highly recommend keeping a periodic job running to check if the service becomes available at a later point in time. The availability of the service may change with Android version upgrades, app upgrades, installs, and uninstalls. By ensuring periodic job checks at a certain time interval, data can be published once the service becomes available. + +#### `publishRecommendationClusters` + +This API is used to publish a list `RecommendationCluster` objects. + +A `RecommendationCluster` object can have the following attributes: + +| Attribute | Requirement | Description | +|---|---|---| +| List of SocialPostEntity, or PortraitMediaEntity | **Required** | A list of entities that make up the recommendations for this Recommendation Cluster. Entities in a single cluster must be of the same type. | +| Title | **Required** | The title for the Recommendation Cluster (for example, *Latest from your friends*). **Recommended text size: under 25 chars** (Text that is too long may show ellipses) | +| Subtitle | Optional | The subtitle for the Recommendation Cluster. | +| Action Uri | Optional | The deep link to the page in the partner app where users can see the complete list of recommendations. Note: You can use deep links for attribution. [Refer to this FAQ](https://developer.android.com/guide/playcore/engage/faq#deeplinks-attribution) | + +> [!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. + +> [!IMPORTANT] +> **Important:** For social apps, it's critical to update recommendations 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. + +### Kotlin + + client.publishRecommendationClusters( + PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()) + +### Java + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster( + new RecommendationCluster.Builder() + .addEntity(entity1) + .addEntity(entity2) + .setTitle("Latest from your friends") + .build()) + .build()); + +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. + +#### `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) + +The following metadata is part of the Sign In Card - + +| Attribute | Requirement | Description | +|---|---|---| +| Action Uri | Required | Deeplink to Action (i.e. navigates to app sign in page) | +| Image | Optional - If not provided, Title must be provided | Image Shown on the Card 16x9 aspect ratio images with a resolution of 1264x712 | +| Title | Optional - If not provided, Image must be provided | Title on the Card | +| Action Text | Optional | Text Shown on the CTA (i.e. Sign in) | +| Subtitle | Optional | Optional Subtitle on the Card | + +> [!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. + +### Kotlin + + var SIGN_IN_CARD_ENTITY = + SignInCardEntity.Builder() + .addPosterImage( + Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build() + + client.publishUserAccountManagementRequest( + PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +### Java + + SignInCardEntity SIGN_IN_CARD_ENTITY = + new SignInCardEntity.Builder() + .addPosterImage( + new Image.Builder() + .setImageUri(Uri.parse("http://www.x.com/image.png")) + .setImageHeightInPixel(500) + .setImageWidthInPixel(500) + .build()) + .setActionText("Sign In") + .setActionUri(Uri.parse("http://xx.com/signin")) + .build(); + + client.publishUserAccountManagementRequest( + new PublishUserAccountManagementRequest.Builder() + .setSignInCardEntity(SIGN_IN_CARD_ENTITY) + .build()); + +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. + +#### `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 : + +- 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. +- It helps developers provide insights into when the data is published versus not. +- Google may use the status codes to nudge the user to do certain actions in the app so they can see the app content or overcome it. + +The list of eligible publish status codes are : + + // Content is published + AppEngagePublishStatusCode.PUBLISHED, + + // Content is not published as user is not signed in + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN, + + // Content is not published as user is not subscribed + AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SUBSCRIPTION, + + // Content is not published as user location is ineligible + AppEngagePublishStatusCode.NOT_PUBLISHED_INELIGIBLE_LOCATION, + + // Content is not published as there is no eligible content + AppEngagePublishStatusCode.NOT_PUBLISHED_NO_ELIGIBLE_CONTENT, + + // Content is not published as the feature is disabled by the client + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_FEATURE_DISABLED_BY_CLIENT, + + // Content is not published as the feature due to a client error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_CLIENT_ERROR, + + // Content is not published as the feature due to a service error + // Available in v1.3.1 + AppEngagePublishStatusCode.NOT_PUBLISHED_SERVICE_ERROR, + + // Content is not published due to some other reason + // 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** + +### Kotlin + + client.updatePublishStatus( + PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()) + +### Java + + client.updatePublishStatus( + new PublishStatusRequest.Builder() + .setStatusCode(AppEngagePublishStatusCode.NOT_PUBLISHED_REQUIRES_SIGN_IN) + .build()); + +#### `deleteRecommendationClusters` + +This API is used to delete the content of Recommendation Clusters. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteRecommendationClusters() + +### Java + + 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. + +#### `deleteUserManagementCluster` + +This API is used to delete the content of UserAccountManagement Cluster. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteUserManagementCluster() + +### Java + + 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. + +#### `deleteClusters` + +This API is used to delete the content of a given cluster type. + +> [!IMPORTANT] +> **Important:** Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. Reach out to [`engage-developers@google.com`](mailto:engage-developers@google.com) before using delete APIs. + +### Kotlin + + client.deleteClusters( + DeleteClustersRequest.Builder() + .addClusterType(ClusterType.TYPE_RECOMMENDATION) + ... + .build()) + +### Java + + client.deleteClusters( + new DeleteClustersRequest.Builder() + .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. + +#### 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. + + client.publishRecommendationClusters( + new PublishRecommendationClustersRequest.Builder() + .addRecommendationCluster(...) + .build()) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + // do something + } else { + Exception exception = task.getException(); + if (exception instanceof AppEngageException) { + @AppEngageErrorCode + int errorCode = ((AppEngageException) exception).getErrorCode(); + if (errorCode == AppEngageErrorCode.SERVICE_NOT_FOUND) { + // do something + } + } + } + }); + +The error is returned as an `AppEngageException` with the cause included as an error code. + +| Error code | Error name | Note | +|---|---|---| +| `1` | `SERVICE_NOT_FOUND` | The service is not available on the given device. | +| `2` | `SERVICE_NOT_AVAILABLE` | The service is available on the given device, but it is not available at the time of the call (for example, it is explicitly disabled). | +| `3` | `SERVICE_CALL_EXECUTION_FAILURE` | The task execution failed due to threading issues. In this case, it can be retried. | +| `4` | `SERVICE_CALL_PERMISSION_DENIED` | The caller is not allowed to make the service call. | +| `5` | `SERVICE_CALL_INVALID_ARGUMENT` | The request contains invalid data (for example, more than the allowed number of clusters). | +| `6` | `SERVICE_CALL_INTERNAL` | There is an error on the service side. | +| `7` | `SERVICE_CALL_RESOURCE_EXHAUSTED` | The service call is made too frequently. | + +### 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. + +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. + +### Kotlin + + class AppEngageBroadcastReceiver : BroadcastReceiver(){ + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION + // broadcast is received + } + + fun registerBroadcastReceivers(context: Context){ + var context = context + context = context.applicationContext + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(AppEngageBroadcastReceiver(), + IntentFilter(Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*scheduler=*/null) + } + +### Java + + class AppEngageBroadcastReceiver extends BroadcastReceiver { + // Trigger recommendation cluster publish when PUBLISH_RECOMMENDATION broadcast + // is received + } + + public static void registerBroadcastReceivers(Context context) { + + context = context.getApplicationContext(); + + // Register Recommendation Cluster Publish Intent + context.registerReceiver(new AppEngageBroadcastReceiver(), + new IntentFilter(com.google.android.engage.service.Intents.ACTION_PUBLISH_RECOMMENDATION), + com.google.android.engage.service.BroadcastReceiverPermissions.BROADCAST_REQUEST_DATA_PUBLISH_PERMISSION, + /*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. + + + + + + + + + +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). + +## 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. + +## Next steps + +After completing this integration, your next steps are as follows: + +- Send an email to [`engage-developers@google.com`](mailto:engage-developers@google.com) and attach your integrated APK that is ready for testing by Google. +- Google performs a verification and reviews internally to make sure the integration works as expected. If changes are needed, Google contacts you with any necessary details. +- When testing is complete and no changes are needed, Google contacts you to notify you that you can start publishing the updated and integrated APK to the Play Store. +- After Google has confirmed that your updated APK has been published to the Play Store, your **Recommendation**, clusters will be published and visible to users. \ No newline at end of file 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 d9a3507..aab021e 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,82 +1,44 @@ -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 @@ -86,40 +48,21 @@ This integration includes the following cluster types: **Recommendation** , - 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. @@ -136,11 +79,9 @@ 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 | |---|---|---|---| @@ -151,8 +92,7 @@ cluster types: ### 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` @@ -274,9 +214,7 @@ 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 | |---|---|---|---| @@ -514,8 +452,7 @@ 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* @@ -532,15 +469,13 @@ 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` @@ -560,33 +495,25 @@ categories. This maps the content with some of the predefined categories namely: - `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. @@ -608,16 +535,29 @@ 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -677,25 +617,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -769,14 +695,12 @@ 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` @@ -807,14 +731,12 @@ 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` @@ -851,14 +773,12 @@ 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` @@ -889,20 +809,16 @@ 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 - @@ -955,20 +871,16 @@ 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. @@ -1008,11 +920,7 @@ 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 @@ -1043,9 +951,7 @@ 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. @@ -1065,9 +971,7 @@ 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. @@ -1087,9 +991,7 @@ 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. @@ -1109,9 +1011,7 @@ 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. @@ -1131,9 +1031,7 @@ 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. @@ -1165,15 +1063,11 @@ 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 @@ -1217,8 +1111,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -1232,23 +1125,13 @@ error code. ### 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 @@ -1336,10 +1219,7 @@ 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. @@ -1372,19 +1251,15 @@ service: ## 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 555b293..dd88e0e 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,19 +1,10 @@ -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 a2ce68c..5ef572e 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,17 +1,11 @@ -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 @@ -26,17 +20,13 @@ 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. @@ -45,11 +35,9 @@ content, provide these essential details: > [!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. @@ -83,9 +71,7 @@ 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: @@ -158,9 +144,7 @@ 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. @@ -175,8 +159,7 @@ 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() @@ -211,11 +194,9 @@ 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() @@ -224,35 +205,22 @@ described in the [Getting Started guide](https://developer.android.com/guide/pla .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: @@ -266,8 +234,7 @@ maintained. .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 @@ -279,8 +246,7 @@ maintained. .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. @@ -293,13 +259,11 @@ maintained. ### 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: @@ -308,12 +272,9 @@ implementation. Verify data accuracy and proper functionality before launch. - 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. @@ -331,23 +292,15 @@ implementation. Verify data accuracy and proper functionality before launch. 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 49d8975..578edc4 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,11 +1,8 @@
-[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) @@ -15,16 +12,13 @@ of the TV features: 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 @@ -42,18 +36,19 @@ Before you begin, complete the following steps: -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 in - the `AndroidManifest.xml` file. +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 ``. - + + + + + > [!IMPORTANT] > **Important:** The Engage service environment declaration is only needed for your release build. For your local testing with the verification app, don't include this element. @@ -62,8 +57,7 @@ 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. val client = AppEngagePublishClient(context) @@ -77,8 +71,7 @@ service is available before publishing. ### 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") @@ -86,19 +79,15 @@ optionally a profile ID and locale. .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 a2fd19b..404104f 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,11 +1,6 @@ -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 @@ -21,22 +16,17 @@ 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 @@ -73,8 +63,7 @@ cluster will be displayed. .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. @@ -84,27 +73,17 @@ transaction: ### 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, @@ -129,8 +108,7 @@ The following code removes recommendations on logout. ## 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) @@ -140,27 +118,21 @@ entities are supported for the Recommendation cluster: ### 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 @@ -198,12 +170,7 @@ repeat it for every platform. ### 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");) @@ -222,9 +189,7 @@ pixels may not be shown. ### 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 @@ -271,11 +236,7 @@ the user. ### 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() @@ -290,12 +251,7 @@ availability windows. ### 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" @@ -314,9 +270,7 @@ 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(); @@ -345,9 +299,7 @@ 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() @@ -372,8 +324,7 @@ which may be used by Google TV for enhanced displays or filtering purposes. ### `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) @@ -381,9 +332,7 @@ fields: .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") @@ -394,8 +343,7 @@ filtering purposes. ### `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") @@ -409,8 +357,7 @@ This represents a live TV channel. Here's an example of creating a .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") @@ -426,9 +373,7 @@ recommendation reason. ### `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 @@ -450,8 +395,7 @@ with all the required fields: .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 3dc495e..4507511 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,18 +1,9 @@ > [!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. @@ -21,35 +12,21 @@ existing Google surfaces. ### 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). @@ -64,16 +41,13 @@ 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 | |---|---|---| @@ -82,16 +56,13 @@ cluster types: | 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 | |---|---| @@ -123,9 +94,7 @@ 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 @@ -147,8 +116,7 @@ cluster: ### 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) @@ -262,8 +230,7 @@ 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 | |---|---|---| @@ -344,13 +311,9 @@ 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` @@ -366,16 +329,29 @@ APIs are available 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) -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. +
+ +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/engage-sdk-integration) + +### Engage SDK integration + +To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill engage-sdk-integration + +If your team uses AI coding tools (such as Gemini in Android Studio), you can automate this migration by prompting your AI assistant: + + Use the engage-sdk-integration skill to use Engage SDK 1.6.0 and refactor isServiceAvailable to pass ServiceAvailabilityRequest for publishing all cluster types. + +
+ +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 | |---|---|---| @@ -433,25 +409,11 @@ required for `ServiceAvailabilityRequest` from the following table. ###### 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) @@ -525,14 +487,12 @@ 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` @@ -563,14 +523,12 @@ 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` @@ -601,20 +559,16 @@ 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 - @@ -667,21 +621,16 @@ 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. @@ -721,11 +670,7 @@ 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 @@ -756,9 +701,7 @@ 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. @@ -778,9 +721,7 @@ 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. @@ -800,9 +741,7 @@ 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. @@ -822,9 +761,7 @@ 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. @@ -854,15 +791,11 @@ 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 @@ -906,8 +839,7 @@ that a follow-up action can be taken to recover and resubmit an successful task. } }); -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 | |---|---|---| @@ -921,23 +853,13 @@ error code. ### 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 @@ -1008,10 +930,7 @@ 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. @@ -1040,19 +958,15 @@ service: ## 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/patterns.md b/play/engage-sdk-integration/references/patterns.md index 099c410..76d8adc 100644 --- a/play/engage-sdk-integration/references/patterns.md +++ b/play/engage-sdk-integration/references/patterns.md @@ -1,12 +1,10 @@ ## 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 @@ -65,15 +63,16 @@ 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 @@ -90,12 +89,14 @@ Add the `` tag inside the `` block in + ```
## EngageWorker +
```kotlin import android.content.Context @@ -294,12 +295,14 @@ class EngageWorker(context: Context, workerParams: WorkerParameters) : Coroutine } } } + ```
## ClusterRequestFactory +
```kotlin class ClusterRequestFactory(context: Context) { @@ -353,12 +356,14 @@ class ClusterRequestFactory(context: Context) { .setSignInCardEntity(signInCard) .build() } + ```
## EngagePublisher +
```kotlin object EngagePublisher { @@ -378,12 +383,14 @@ object EngagePublisher { WorkManager.getInstance(context).enqueueUniqueWork("EngageOneTime", ExistingWorkPolicy.REPLACE, workRequest) } } + ```
## Constants +
```kotlin object Constants { @@ -399,12 +406,14 @@ object Constants { // const val PUBLISH_TYPE_FOOD_SHOPPING_CARD = "FOOD_SHOPPING_CARD" // const val PUBLISH_TYPE_RESERVATION = "RESERVATION" } + ```
## ItemToEntityConverter +
```kotlin object ItemToEntityConverter { @@ -419,22 +428,16 @@ 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" @@ -455,10 +458,9 @@ project. ## 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 { @@ -474,27 +476,30 @@ 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( @@ -507,12 +512,14 @@ val platformSpecificPlaybackUris = listOf( .setActionUri(Uri.parse("https://www.example.com/mobile/play/123")) .build() ) + ```
### AccountProfile example +
```kotlin val accountProfile: AccountProfile @@ -522,6 +529,7 @@ 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 00849e6..f841032 100644 --- a/play/engage-sdk-integration/references/requests.md +++ b/play/engage-sdk-integration/references/requests.md @@ -1,5 +1,4 @@ -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": { diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md index 6784606..8c96d85 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-07-02' + last-updated: '2026-08-06' keywords: - android - play billing @@ -20,8 +20,7 @@ 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 @@ -29,22 +28,18 @@ upgrade Play Billing Library to the latest version." 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). @@ -53,9 +48,7 @@ instructions from: ## 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 @@ -64,8 +57,7 @@ accomplished afterwards.* ### 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 462f373..1d1105c 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 @@ -23,37 +23,21 @@ 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) @@ -74,8 +58,7 @@ 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 @@ -109,46 +92,29 @@ 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 @@ -156,137 +122,87 @@ available. - 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 @@ -294,8 +210,7 @@ available. ## 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 @@ -307,8 +222,7 @@ available. ## 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 @@ -325,8 +239,7 @@ available. ## 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 @@ -334,65 +247,37 @@ 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 @@ -400,8 +285,7 @@ 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 @@ -414,8 +298,7 @@ available. ## 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. @@ -427,8 +310,7 @@ 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. @@ -436,75 +318,46 @@ 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 @@ -517,8 +370,7 @@ plugin are now available. ## 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 @@ -527,8 +379,7 @@ available. ## 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 @@ -536,8 +387,7 @@ available. ## 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 @@ -563,23 +413,13 @@ 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 @@ -587,27 +427,19 @@ Unity](https://developer.android.com/google/play/billing/unity). - 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) @@ -619,14 +451,11 @@ 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 @@ -635,8 +464,7 @@ contains the following changes. ## 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 @@ -646,32 +474,11 @@ contains the following changes. > [!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: @@ -681,32 +488,18 @@ 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. @@ -716,11 +509,9 @@ method. > [!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. @@ -729,78 +520,42 @@ new methods for retrieving the original SKU price: ### 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 @@ -808,8 +563,7 @@ locale and is not meant to be shown to end users. ## 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 @@ -822,14 +576,11 @@ contains the following changes. ## 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 @@ -843,13 +594,11 @@ contains the following changes. ## 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()`. @@ -857,14 +606,9 @@ contains the following changes. #### 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 @@ -900,32 +644,19 @@ 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 @@ -984,8 +715,7 @@ 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 @@ -995,13 +725,11 @@ contains the following changes. ### 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 @@ -1040,20 +768,11 @@ 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 @@ -1063,8 +782,7 @@ is kept as well. ## 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 @@ -1075,13 +793,11 @@ contains the following changes. ### 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 @@ -1097,10 +813,7 @@ 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 @@ -1128,10 +841,7 @@ 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 @@ -1153,10 +863,7 @@ 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 @@ -1172,10 +879,7 @@ 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 227a537..b0a095a 100644 --- a/play/play-billing-library-version-upgrade/references/migration-logic.md +++ b/play/play-billing-library-version-upgrade/references/migration-logic.md @@ -2,8 +2,7 @@ ### 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`. @@ -11,8 +10,7 @@ library-managed state. ### 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). @@ -20,13 +18,7 @@ and refined in v8. - **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) @@ -44,8 +36,7 @@ and refined in v8. ### 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). @@ -69,8 +60,7 @@ checks. ### 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 98f5831..47005eb 100644 --- a/play/play-billing-library-version-upgrade/references/version-checklist.md +++ b/play/play-billing-library-version-upgrade/references/version-checklist.md @@ -1,7 +1,6 @@ ## 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 a94df09..b1f5f42 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-06-25' + last-updated: '2026-08-06' keywords: - recipe - Android @@ -24,9 +24,7 @@ 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 @@ -84,45 +82,19 @@ 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. } *** ** * ** *** @@ -130,14 +102,14 @@ MUST verify that the calling package signature fingerprint matches your trusted ### 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() { @@ -179,20 +151,21 @@ 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() { @@ -221,27 +194,30 @@ 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) { @@ -286,6 +263,7 @@ override fun onNewIntent(newIntent: Intent) { private fun validateIntent(intent: Intent): Boolean { return intent.hasExtra("VALID_PAYLOAD_MARKER") } + ```
@@ -299,6 +277,7 @@ 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) { @@ -323,14 +302,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. @@ -340,6 +319,7 @@ injection. - **Expected Outputs:** - `Cursor`: Filtered query results, strictly bound to projection maps. +
```xml + ```
+
```kotlin override fun query( @@ -378,6 +360,7 @@ override fun query( val db = dbHelper.readableDatabase return queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder) } + ```
@@ -391,6 +374,7 @@ 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() { @@ -439,6 +423,7 @@ class SecureBoundService : Service() { } } } + ```
@@ -447,9 +432,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?) { @@ -469,6 +454,7 @@ fun safeErrorHandling(callingPackage: String?) { Log.w("INTEGRITY_WARNING", "Missing intent parameter", e) } } + ```
@@ -485,10 +471,7 @@ 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 f618ab2..ef19d2c 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-04-01' + last-updated: '2026-08-06' 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,6 +51,7 @@ metadata: contentPadding = innerPadding ) { /* Content */ } } + ```
@@ -76,6 +77,7 @@ 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( @@ -90,16 +92,14 @@ 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,16 +109,13 @@ 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 @@ -130,15 +127,14 @@ 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 @@ -151,15 +147,14 @@ 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 @@ -172,16 +167,14 @@ 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 @@ -193,15 +186,14 @@ 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 @@ -212,6 +204,7 @@ Scaffold() { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } + ```
@@ -222,7 +215,6 @@ Scaffold() { innerPadding -> The following code samples WILL NOT cause excessive padding. - ```kotlin // RIGHT Box( @@ -233,12 +225,14 @@ Box( modifier = Modifier.imePadding() ) { /* Content */ } } + ```
*** ** * ** *** +
```kotlin // RIGHT @@ -250,12 +244,14 @@ Box( modifier = Modifier.imePadding() ) { /* Content */ } } + ```
*** ** * ** *** +
```kotlin // RIGHT @@ -269,15 +265,14 @@ 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 @@ -289,19 +284,16 @@ 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`. @@ -325,22 +317,19 @@ 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() { @@ -387,20 +376,18 @@ 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( @@ -412,6 +399,7 @@ Dialog( decorFitsSystemWindows = false ) ) { /* Content */ } + ```
diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md index 31207c7..316e9d4 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-06-25' + last-updated: '2026-08-06' keywords: - android - testing @@ -17,8 +17,7 @@ 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... @@ -40,35 +39,26 @@ dependencies in the libs.versions.toml file, or build files: ## 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. @@ -80,43 +70,30 @@ 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 @@ -128,14 +105,11 @@ 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** @@ -157,25 +131,17 @@ 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 @@ -185,14 +151,8 @@ 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 1bea76b..f0b2f75 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,24 +4,15 @@ 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 { @@ -41,17 +32,13 @@ an empty activity such as [`ComponentActivity`](https://developer.android.com/re } } -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") @@ -64,8 +51,7 @@ 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 @@ -76,12 +62,9 @@ property: ### 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 { @@ -106,40 +89,25 @@ useful to verify the implementation of [`rememberSaveable`](https://developer.an ### 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 { @@ -149,13 +117,14 @@ 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 { @@ -166,6 +135,7 @@ 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 b1f7876..391510e 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,16 +1,9 @@ > [!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: @@ -24,31 +17,30 @@ 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. @@ -57,8 +49,7 @@ project must meet the following requirements: - 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. @@ -71,22 +62,19 @@ the requirements are as follows: ## 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: @@ -98,16 +86,13 @@ Preview Screenshot Testing plugin. To set up the plugin, follow these steps: [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: @@ -124,15 +109,11 @@ Preview Screenshot Testing plugin. To set up the plugin, follow these steps: ## 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 @@ -152,16 +133,11 @@ this file or other files created in the same source set. ## 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 @@ -170,46 +146,36 @@ 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 @@ -217,11 +183,8 @@ adding the following property to your `gradle.properties` file: - **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 022bf07..e1fe4ac 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,12 +1,8 @@ -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 @@ -26,21 +22,15 @@ 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 { @@ -64,11 +54,9 @@ 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 @@ -85,36 +73,21 @@ 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. @@ -129,10 +102,7 @@ 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 { @@ -145,13 +115,11 @@ 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 @@ -167,13 +135,11 @@ 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: @@ -208,14 +174,9 @@ 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 @@ -230,10 +191,7 @@ 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 @@ -251,19 +209,13 @@ 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) @@ -271,8 +223,7 @@ annotation in the test class: 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) @@ -294,11 +245,7 @@ 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. @@ -311,12 +258,9 @@ The recommendation is to use `@TestInstallIn` whenever possible. #### 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) @@ -330,13 +274,9 @@ 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 ... @@ -350,10 +290,7 @@ 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 @@ -361,36 +298,23 @@ 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 @@ -409,8 +333,7 @@ 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 @@ -426,12 +349,9 @@ 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 new file mode 100644 index 0000000..0c46918 --- /dev/null +++ b/tv/leanback-to-compose-tv-migration/SKILL.md @@ -0,0 +1,692 @@ +--- +name: leanback-to-compose-tv-migration +description: Provides instructions and architectural patterns for migrating Android + TV applications from legacy Leanback UI Toolkit, Android Views, or Support Fragments + to Jetpack Compose for TV (androidx.tv). Use this skill for Leanback to Compose + migrations, including browse screen, settings screen, authentication screen, login + screen, or video playback screen migrations, or when replacing BrowseSupportFragment, + LeanbackSettingsFragment, PreferenceFragment, BaseLeanbackPreferenceFragmentCompat, + VideoSupportFragment, GuidedStepSupportFragment, SearchSupportFragment, VerticalGridSupportFragment, + Presenter, ArrayObjectAdapter, or CursorMapper with modern Compose equivalents, + implementing immersive carousels with focus memory, Media3 video playback with PlayerSurface, + or custom 10-foot hero layouts. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-08-06' + keywords: + - Android TV + - Jetpack Compose + - Leanback + - migration + - androidx.tv + - Carousel + - 10-foot UI + - PlayerSurface + - Media3 + - focusRestorer + - FocusRequester + - TV development + - Android Views + - LeanbackSettingsFragment + - PreferenceFragment + - BrowseSupportFragment +--- + +## 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: + +- **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. +- **D-pad navigation** : Interaction relies on a directional remote control with limited 4-way navigation (`Up`, `Down`, `Left`, `Right`) with components organized into clear spatial grids and carousels without focus traps. + +## 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: + +- **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.*`). +- **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. +- **Media3 Compose dependencies** : Include `androidx.media3:media3-ui-compose` in `app/build.gradle` when modernizing media playback screens. +- **Prohibition of legacy AndroidView wrappers** : Don't use legacy `AndroidView` wrappers to embed View-based components into Jetpack Compose screens. All migrated screens must use Compose components or Media3 Compose surfaces (`PlayerSurface`). + +## 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). + +### 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() }`: + +
+ +```kotlin +val focusRequester = remember { FocusRequester() } +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.* + +### 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. + +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( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 48.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.End +) { + Button( + onClick = { /* Search */ }, + 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. + +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( + modifier = Modifier.focusRestorer(), + contentPadding = PaddingValues(horizontal = 48.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) +) { + itemsIndexed(videos) { vidIndex, video -> + CompactCard( + onClick = { onVideoClick(video) }, + image = { + AsyncImage( + model = video.cardImageUrl, + contentDescription = video.title, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + }, + title = { Text(video.title) }, + modifier = Modifier + .then( + if (catIndex == 0 && vidIndex == 0) { + Modifier.focusRequester(firstCardFocusRequester) + } else { + Modifier + } + ) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + focusedVideo = video + focusedCategoryIndex = catIndex + } + } + ) + } +} + +``` + +
+ +### 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). + +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: + +
+ +```kotlin +Surface( + onClick = { focusRequester.requestFocus() }, + border = ClickableSurfaceDefaults.border(focusedBorder = Border(border = BorderStroke(2.dp, Color.White))), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) +) { + OutlinedTextField( + value = username, + onValueChange = { username = it }, + label = { Text("Username") }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), + modifier = Modifier + .fillMaxWidth() + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown && (event.key == Key.Enter || event.key == Key.NumPadEnter)) { + focusManager.moveFocus(FocusDirection.Down) + true + } else false + } + ) +} + +Surface( + onClick = {}, + border = ClickableSurfaceDefaults.border(focusedBorder = Border(border = BorderStroke(2.dp, Color.White))), + modifier = Modifier.fillMaxWidth() +) { + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { onLoginSuccess() }), + modifier = Modifier + .fillMaxWidth() + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown && (event.key == Key.Enter || event.key == Key.NumPadEnter)) { + onLoginSuccess() + true + } else false + } + ) +} + +``` + +
+ +## 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). + +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: + +
+ +```kotlin +@Composable +fun CatalogBrowser( + featuredContentList: List, + sectionList: List
, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(sectionList.size) { index -> + val section = sectionList[index] + SectionRow(section, onItemSelected = onItemSelected) + } + } +} + +``` + +
+ +## 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: + +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`). +3. **Prohibition of legacy AndroidView wrappers** : Don't wrap legacy `PlayerView` or `StyledPlayerView` in `AndroidView { ... }`. You must use `PlayerSurface` (`androidx.media3.ui.compose.PlayerSurface`) with `ExoPlayer` for video rendering in Compose for TV. + +## Phased migration strategy + +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 +- **Phase 3**: Core browsing and discovery screens +- **Phase 4**: Details and media playback +- **Phase 5**: Final unification and cleanup + +### Phase 1: Foundation and design system + +- Create `TvTheme.kt` wrapping `TvMaterialTheme` with custom `ColorScheme`, `Typography`, and `Shapes`. +- Build atomic reusable components: `MovieCard`, `SectionHeader`, `LoadingIndicator`, `ErrorState`. + +### Phase 2: Leaf and standalone screens + +- Migrate screens with no outbound navigation first, such as error messages, onboarding screens, or settings screens, by replacing them with `ErrorDialog`, `OnboardingScreen`, and `SettingsScreen`. +- Replace anything using `BaseLeanbackPreferenceFragmentCompat`, `BaseLeanbackPreferenceFragment`, or `LeanbackSettingsFragment` to use Compose, for example by using `ListItem` + `Switch` bound directly to `SharedPreferences` and replacing the `findPreference` implementation. +- Ensure screens receive initial D-pad focus on the first or main component of that screen using `FocusRequester`, for example on the first `ListItem`. +- 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 +fun TvSettingsScreen( + modifier: Modifier = Modifier +) { + var autoPlayNext by remember { mutableStateOf(true) } + var highQualityAudio by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxSize() + .padding(48.dp) + ) { + Text( + text = "Settings", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 24.dp) + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + item { + ListItem( + selected = false, + onClick = { autoPlayNext = !autoPlayNext }, + headlineContent = { Text("Autoplay Next Video") }, + supportingContent = { Text("Automatically start playing next item in queue") }, + trailingContent = { + Switch( + checked = autoPlayNext, + onCheckedChange = null + ) + } + ) + } + + item { + ListItem( + selected = false, + onClick = { highQualityAudio = !highQualityAudio }, + headlineContent = { Text("High Quality Audio") }, + supportingContent = { Text("Use spatial audio and multi-channel output when available") }, + trailingContent = { + Switch( + checked = highQualityAudio, + onCheckedChange = null + ) + } + ) + } + } + } +} + +``` + +
+ +### Phase 3: Core browsing and discovery screens + +- Migrate `VerticalGridScreen` (`LazyVerticalGrid`), `SearchScreen` (`BasicTextField` with live list filtering), and `BrowseScreen` (`LazyColumn` of `LazyRow`s). +- Eradicate legacy `ArrayObjectAdapter`, `ListRowPresenter`, `CardPresenter`, and `HeaderItem` classes. + +### Phase 4: Details and media playback + +- Migrate `VideoDetailsScreen` and `GuidedStepScreen`. +- Migrate `PlaybackScreen` using Compose Media3 `PlayerSurface` (`androidx.media3.ui.compose.PlayerSurface`) paired with a translucent bottom overlay containing Media3 Compose transport controls (such as `PlayPauseButton`, `SeekBackButton`, `SeekForwardButton`). + +### Phase 5: Final unification and cleanup + +- Remove all remaining legacy `.java` activities, fragments, presenters, and XML layout files. +- Ensure all activities extend `ComponentActivity` or `FragmentActivity` calling `setContent { ... }`. +- Completely remove legacy Leanback themes and style declarations from `res/values/styles.xml` and `res/values/themes.xml` (for example, any styles inheriting from `Theme.Leanback` or referencing `lb_` styles). + +## Component and class mapping guide + +| Legacy Leanback / View Class | Modern Jetpack Compose Equivalent | +|---|---| +| `BrowseSupportFragment` / `MainFragment` | `BrowseScreen` (`LazyColumn` containing categorized `LazyRow`s + Hero Banner) | +| `DetailsSupportFragment` | `VideoDetailsScreen` (Poster image, text column, action buttons, related `LazyRow`) | +| `VideoSupportFragment` / `PlaybackGlue` | `PlaybackScreen` (Media3 `ExoPlayer` + Compose `PlayerSurface`) | +| `GuidedStepSupportFragment` | `GuidedStepScreen` (Split-screen layout: 40% left guidance pane, 60% right actions pane) | +| `SearchSupportFragment` | `SearchScreen` (`BasicTextField` + live filtering + `LazyVerticalGrid`) | +| `VerticalGridSupportFragment` | `VerticalGridScreen` (`LazyVerticalGrid(columns = GridCells.Fixed(5))`) | +| `ArrayObjectAdapter` / `Presenter` | Declarative `@Composable` functions observing immutable `State>` | +| `CursorMapper` / `LoaderManager` / `CursorLoader` | Kotlin Coroutines / `withContext(Dispatchers.IO)` in a Repository object | +| `OnboardingSupportFragment` | `OnboardingScreen` (`HorizontalPager` + D-Pad navigation buttons) | +| `LeanbackSettingsFragment` / `PreferenceFragment` | `SettingsScreen` (`FocusRequester` on first `ListItem` + trailing `Switch` bound to `SharedPreferences`) | + +## Reference implementations and battle-tested patterns + +### 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. + +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 +fun ImmersiveBrowseScreen( + categories: Map>, + onVideoClick: (Video) -> Unit +) { + var focusedVideo by remember { mutableStateOf(null) } + var focusedCategoryIndex by remember { mutableStateOf(0) } + val listState = rememberLazyListState() + val topBarFocusRequester = remember { FocusRequester() } + val firstCardFocusRequester = remember { FocusRequester() } + + Box(modifier = Modifier.fillMaxSize()) { + AsyncImage( + model = focusedVideo?.bgImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + PositionFocusedItemInLazyLayout(parentFraction = 0.35f, childFraction = 0.5f) { + LazyColumn( + state = listState, + contentPadding = PaddingValues(top = 36.dp, bottom = 64.dp), + verticalArrangement = Arrangement.spacedBy(28.dp), + modifier = Modifier.fillMaxSize() + ) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 48.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.End + ) { + Button( + onClick = { /* Search */ }, + modifier = Modifier.focusRequester(topBarFocusRequester) + ) { Text("Search") } + } + } + + itemsIndexed(categories.entries.toList()) { catIndex, (categoryName, videos) -> + Column { + if (catIndex == 0 && focusedCategoryIndex == 0) { + Column( + modifier = Modifier + .heightIn(min = 200.dp) + .padding(horizontal = 48.dp) + ) { + Text( + text = focusedVideo?.title ?: "", + style = MaterialTheme.typography.displayMedium + ) + Text( + text = focusedVideo?.description ?: "", + style = MaterialTheme.typography.bodyLarge + ) + } + } else { + Text( + text = categoryName, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 48.dp, vertical = 8.dp) + ) + } + + LazyRow( + modifier = Modifier.focusRestorer(), + contentPadding = PaddingValues(horizontal = 48.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + itemsIndexed(videos) { vidIndex, video -> + CompactCard( + onClick = { onVideoClick(video) }, + image = { + AsyncImage( + model = video.cardImageUrl, + contentDescription = video.title, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + }, + title = { Text(video.title) }, + modifier = Modifier + .then( + if (catIndex == 0 && vidIndex == 0) { + Modifier.focusRequester(firstCardFocusRequester) + } else { + Modifier + } + ) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + focusedVideo = video + focusedCategoryIndex = catIndex + } + } + ) + } + } + } + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun PositionFocusedItemInLazyLayout( + parentFraction: Float = 0.35f, + childFraction: Float = 0.5f, + content: @Composable () -> Unit, +) { + val bringIntoViewSpec = remember(parentFraction, childFraction) { + object : BringIntoViewSpec { + override fun calculateScrollDistance( + offset: Float, + size: Float, + containerSize: Float + ): Float { + if (offset >= 0f && offset <= containerSize * 0.45f) { + return 0f + } + val initialTargetForLeadingEdge = parentFraction * containerSize - (childFraction * size) + val targetForLeadingEdge = if (size <= containerSize && (containerSize - initialTargetForLeadingEdge) < size) { + containerSize - size + } else { + initialTargetForLeadingEdge + } + return offset - targetForLeadingEdge + } + } + } + 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: + +
+ +```kotlin +@OptIn(UnstableApi::class) +@Composable +fun Media3PlaybackScreen( + video: Video, + onFinish: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val exoPlayer = remember(context) { ExoPlayer.Builder(context).build() } + var showControls by remember { mutableStateOf(true) } + val focusRequester = remember { FocusRequester() } + val coroutineScope = rememberCoroutineScope() + var autoHideJob by remember { mutableStateOf(null) } + + fun scheduleAutoHide() { + autoHideJob?.cancel() + autoHideJob = coroutineScope.launch { + delay(5000) + showControls = false + } + } + + DisposableEffect(exoPlayer) { + val listener = object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_ENDED) { + onFinish() + } + } + } + exoPlayer.addListener(listener) + onDispose { + autoHideJob?.cancel() + exoPlayer.removeListener(listener) + exoPlayer.release() + } + } + + LaunchedEffect(video) { + focusRequester.requestFocus() + scheduleAutoHide() + + val mediaItem = MediaItem.fromUri(video.videoUrl) + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.playWhenReady = true + } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.Black) + .focusRequester(focusRequester) + .focusable() + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown) { + when (event.key) { + Key.DirectionCenter, Key.Enter, Key.NumPadEnter, Key.MediaPlayPause, Key.Spacebar -> { + if (exoPlayer.isPlaying) exoPlayer.pause() else exoPlayer.play() + showControls = true + scheduleAutoHide() + true + } + Key.DirectionLeft, Key.MediaRewind -> { + val newPos = (exoPlayer.currentPosition - 10_000L).coerceAtLeast(0L) + exoPlayer.seekTo(newPos) + showControls = true + scheduleAutoHide() + true + } + Key.DirectionRight, Key.MediaFastForward -> { + val duration = exoPlayer.duration.coerceAtLeast(0L) + val newPos = (exoPlayer.currentPosition + 10_000L).coerceAtMost(duration) + exoPlayer.seekTo(newPos) + showControls = true + scheduleAutoHide() + true + } + Key.DirectionUp, Key.DirectionDown -> { + showControls = !showControls + if (showControls) scheduleAutoHide() else autoHideJob?.cancel() + true + } + else -> false + } + } else false + }, + contentAlignment = Alignment.Center + ) { + PlayerSurface( + player = exoPlayer, + modifier = Modifier.fillMaxSize() + ) + + if (showControls) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x80000000)) + .padding(48.dp), + contentAlignment = Alignment.BottomCenter + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SeekBackButton(player = exoPlayer) + PlayPauseButton(player = exoPlayer) + SeekForwardButton(player = exoPlayer) + } + } + } + } +} + +``` + +
+ +### 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: + +
+ +```kotlin +object VideoFlowRepository { + fun getVideosFlow(context: Context, contentUri: Uri): Flow> = callbackFlow { + val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + trySend(queryVideos(context, contentUri)) + } + } + + context.contentResolver.registerContentObserver( + contentUri, + true, + contentObserver + ) + + val initialVideos = queryVideos(context, contentUri) + trySend(initialVideos) + + awaitClose { + context.contentResolver.unregisterContentObserver(contentObserver) + } + }.flowOn(Dispatchers.IO) + + private fun queryVideos(context: Context, uri: Uri): List