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,