# Storyteller Kotlin SDK Public Docs (AI bundle) This bundle contains AI-friendly Markdown for the public Storyteller Kotlin SDK docs. Use the delimiters to navigate to a specific page. ## Bundle index - Quickstart Guide (slug: getting-started) - Additional Methods (slug: additional-methods) - Working with Users (slug: users) - Storyteller Lists (slug: storyteller-lists) - Storyteller List Views (Legacy) (slug: storyteller-list-views) - Embedded Clips (slug: storyteller-clips-fragment) - Storyteller Home (slug: storyteller-home) - Custom Themes (slug: themes) - Storyteller Delegates (slug: storyteller-delegates) - Storyteller Module (slug: storyteller-module) - Navigating to App (slug: navigating-to-app) - Analytics (slug: analytics) - Ads (slug: ads) - Privacy and Tracking (slug: privacy-and-tracking) - Deep Linking (slug: deep-linking) - Open Player (slug: open-player) - Search (slug: search) - Storyteller Brightcove Module (slug: storyteller-brightcove) - Cards (slug: cards) - Open Source Licenses (slug: open-source-licenses) --- # Quickstart Guide Source: public-docs/GettingStarted.md ## Summary - Add the Storyteller Maven repo and SDK dependency via Gradle. - Initialize Storyteller with API key, user input, and tracking options. - Add the StorytellerStoriesRow composable to render story lists. - XML list views are supported but legacy. This is a developers' guide for setting up Storyteller for native Android apps. This guide will cover the basic technical steps for initializing the Storyteller SDK, authenticating a user, and adding a `StorytellerStoriesRowView` to your app. ## Resources - [Storyteller Showcase App](https://github.com/getstoryteller/storyteller-showcase-android) ### Showcase examples - [Compose — SDK init in `ShowcaseApp`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/main/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L39) - [Compose — `Storyteller.initialize` in `StorytellerServiceImpl`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L40) - [XML — SDK init in `ShowcaseApp`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L40) - [XML — `Storyteller.initialize` in `StorytellerServiceImpl`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L43) ## How to Add the SDK to Your Project Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`. ### SDK Dependencies Currently the Android SDK contains the following [dependencies](https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk/Storyteller/sdk/11.1.0/sdk-11.1.0.pom). ### R8 / ProGuard If your app uses R8, the rules are added automatically. ### SDK Installation The Android SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/). 1. Add the maven repository for the SDK in the _Project_ `build.gradle` file (`MyAwesomeApp/build.gradle`), under the `allprojects` section > Note: make sure it is added to `allprojects`, and not `buildscript` ```groovy ... allprojects { repositories { google() mavenCentral() maven { url 'https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk' } } } ``` 1. Modify the app _Module_ `build.gradle` file (`MyAwesomeApp/app/build.gradle`) ```groovy ... dependencies { def storyteller_version = "11.1.0" implementation(group: "Storyteller", name: "sdk", version: "$storyteller_version") } ``` 1. Sync your project with Gradle files - Android Studio ## SDK Initialization Before using the Android SDK in your app, you need to initialize it with an API key. ### Adding an API Key Use the `initialize(apiKey: String, userInput: StorytellerUserInput? = null, eventTrackingOptions: StorytellerEventTrackingOptions = StorytellerEventTrackingOptions(), onSuccess: () -> Unit = {}, onFailure: (Error) -> Unit = {})` public method to manually initialize the SDK at runtime. This will authenticate the SDK on the Storyteller API and configure it with the corresponding settings. - `apiKey`: (**Required**) the API key you wish to initialize the SDK with - `userInput` : details of the user to be authenticated (this should be a unique user ID. If this is not set, the default value is used) - `eventTrackingOptions`: privacy and tracking configuration options (defaults to all tracking enabled) - `onSuccess`: callback for successful completion - `onFailure`: callback for failed completion with error Usage: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true // ... other options ), onSuccess = { // onSuccess action }, onFailure = { error -> // onFailure action } ) ``` Initialization errors: - `InitializationError`: when the context is `null` - `InvalidAPIKeyError`: when an invalid API key is used - `NetworkError`: when the call to load the settings for the SDK fails (i.e. a non-success HTTP status code is returned) - `NetworkTimeoutError`: when the call to load the settings for the SDK times out - `JSONParseError`: when a malformed settings response is received from the server > Note: Please be aware that this method is asynchronous ## Authenticate a User For more information about Users and External IDs, please see Working with Users ## Adding a StorytellerStoriesRow Composable 1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc. ```kotlin @Composable fun MainScreen() { Box() { // ... } } ``` 1. Storyteller Composables Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK. ```kotlin StorytellerStoriesRow( modifier = Modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below. delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below. state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below. ) ``` ## XML Views (Legacy) Prefered way to add Storyteller Lists is to use Composables. Storyteller still supports XML/Views and the guide can be found in the StorytellerListViews documentation. ## AI notes / pitfalls / constraints - R8/ProGuard rules are added automatically. - initialize is asynchronous; use onSuccess/onFailure callbacks for sequencing. - Invalid API keys or network errors surface via onFailure. ## Related topics - Working with Users - in the StorytellerListViews documentation --- # Additional Methods Source: public-docs/AdditionalMethods.md ## Summary - Set a global StorytellerDelegate for SDK callbacks. - Read static properties for SDK state (initialized, search, player visibility, version). - Use helper methods to open search or sheets, dismiss or resume players, and query story or clip counts. ## Showcase examples - [Compose — `Storyteller.isSearchEnabled` (`StorytellerTopAppBar`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/main/StorytellerTopAppBar.kt#L87) - [Compose — `Storyteller.dismissPlayer` before navigation (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L121) - [XML — `Storyteller.dismissPlayer` before navigation (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L86) ## Setting the global StorytellerDelegate Please see the dedicated Storyteller Delegates page. ```kotlin Storyteller.storytellerDelegate = myCustomStorytellerDelegate ``` ## Static Attributes ### isInitialized `isInitialized` is a boolean static property which is set to `true` if Storyteller was initialized successfully. ```kotlin val initialized = Storyteller.isInitialized ``` ### isSearchEnabled `isSearchEnabled` is a boolean static property which is set to `true` if the Search feature is enabled in the Storyteller configuration. ```kotlin val searchEnabled = Storyteller.isSearchEnabled ``` ### isPlayerVisible `isPlayerVisible` is a boolean property which is set to `true` when a Story or Clip player is opened, `false` when the Story or Clip player is dismissed. ```kotlin val isStoryPlayerVisible = Storyteller.isPlayerVisible ``` ### version The `version` is a static String property which holds the SDK version. ```kotlin val storytellerVersion = Storyteller.version ``` ### currentUserId ⚠️ **DEPRECATED** > **⚠️ DEPRECATED:** This property is deprecated and will be removed in version 11.0.0 for VPPA compliance. User IDs should not be exposed to integrators to prevent matching with video viewing data. This method now returns an empty string and will be completely removed in a future version. `currentUserId` was a static String property that previously returned the current user ID. For VPPA compliance, this property has been deprecated and now returns an empty string. ```kotlin // DEPRECATED - Do not use val userId = Storyteller.currentUserId // Always returns "" ``` ### user Allows setting custom attributes for audience targeting. Please see User Customization ```kotlin Storyteller.user.setCustomAttribute('location', 'New York'); ``` ## Static Methods ### openSearch ```kotlin fun openSearch(activity: Activity) ``` This method opens the Search screen. Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Search activity ### openSheet ```kotlin fun openSheet(activity: AppCompatActivity, sheetId: String, onError: (StorytellerError) -> Unit = {}) ``` This call opens a Sheet with a given ID. Parameters: - `activity` - this is the Activity that will be used to launch the Sheet - `sheetId` - this is a Sheet's ID, if it is `null` then `onError` will be called - `onError` - this is called when there is an issue with opening a Sheet (e.g. the sheetId is a blank string) ### dismissPlayer `dismissPlayer(Boolean, String?)`: force closes the currently open Story or Player Views. If no Player is open when this is called, it has no effect. `animated`: the flag indicating if close animation should be triggered. Defaults to `true`. `reason`: the reason why the Story Page was force closed. This will be used to populate the `dismissedReason` parameter of the corresponding `onUserActivityOccurred` callback. If this is set to `null` the `onUserActivityOccurred` callback will not be triggered. `onCompletion`: a callback that will be called when the player is dismissed. This is useful when you want to perform an action after the player is dismissed. ```kotlin Storyteller.dismissPlayer(true, "reason") ``` ### resumePlayer `resumePlayer()`: resumes the currently open Story or Player Views. If no Player is open when this is called, it has no effect. ```kotlin Storyteller.resumePlayer() ``` ### getStoriesCount ```kotlin suspend fun getStoriesCount(categoryIds: List): Int ``` This method returns the count of stories available in the specified categories. It can be used to determine if a particular category or set of categories contains any Stories before rendering a UI component. Parameters: - `categoryIds` - a list of category IDs to check for stories Return value: - Returns the total number of stories available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty ### getClipsCount ```kotlin suspend fun getClipsCount(categoryIds: List): Int ``` This method returns the count of clips available in the specified categories. It can be used to determine if a particular category or set of categories contains any Clips before rendering a UI component. Parameters: - `categoryIds` - a list of category IDs to check for clips Return value: - Returns the total number of clips available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty ## AI notes / pitfalls / constraints - Storyteller.currentUserId is deprecated and returns an empty string. - openSearch/openSheet require an Activity; openSheet calls onError for invalid sheetId. - dismissPlayer and resumePlayer do nothing if no player is open. - getStoriesCount/getClipsCount return 0 when not initialized or when category list is empty. ## Related topics - Storyteller Delegates - User Customization --- # Working with Users Source: public-docs/Users.md ## Summary - Storyteller can manage user IDs automatically or accept a provided externalId. - Initialize with StorytellerUserInput to set a user, and re-initialize to change users. - Set locale and custom attributes via Storyteller.user. - Followed categories APIs manage clip follow state and can be Storyteller-managed. User IDs can be used for reporting purposes, storing the read status of Clips and Stories, followed categories and user preferences. By default, the Storyteller SDK creates an `externalId` for users when the SDK is first initialized. Each time `null` value is passed to `Storyteller.initialize(userInput = null)` SDK will handle user management and the default `externalId` is stored until the user uninstalls the app. **However, if you have a user account system then you may wish to set your own user IDs within the Storyteller SDK. The `externalId` should be an identifier that is unique per user and does not change. Therefore, using something like the user's email address is not a good choice for an `externalId` as the user may change it at some point in the future. However, using a unique UUID/GUID would be a good choice as it is guaranteed to not change over time.** > Note: Storyteller SDK is not storing any user IDs passed to initialize. We follow VPPA compliance and the user IDs are hashed during the App runtime ## Showcase examples - [Compose — user ID, custom attributes, locale (`AccountScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/account/AccountScreen.kt#L65) - [Compose — initialization + user attribute updates (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L105) - [XML — login + verify flow (user ID)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/login/LoginViewModel.kt#L53) - [XML — initialization + user attribute updates (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L81) ## Setting a User ID In order to supply a `externalId` to the Storyteller SDK, call the following method: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", useInput = StorytellerUserInput("unique-externalId"), onSuccess = { // onSuccess action }, onFailure = { error -> // onFailure action } ) ``` This should be called as soon as the value for the `externalId` is known in your app. For example, this could be on app start or when a user logs in. ## Current User Information You can access the following information about the current user via the following properties: ```kotlin Storyteller.currentUserId // Returns the current user id Storyteller.currentApiKey // Returns the current api key ``` ## Changing Users If you use login in your app and wish to allow users to logout and log back in as a new user (or proceed as an anonymous user) then when a user logs out you should call `initialize` again, specifying a new `externalId`. Note that this will reset the local store of which Pages the user has viewed. ## Setting the User's Locale In order to set the locale to the Storyteller SDK, call the following method: ```kotlin Storyteller.user.setLocale(locale: String?) ``` Ensure the locale parameter uses an ISO 639-1 two-letter if available, otherwise, use an ISO 639-2 three-letter language code for precise language specification: ```kotlin Storyteller.user.setLocale("fr") Storyteller.user.setLocale("ace") ``` To clear or reset the locale settings, simply pass `null` as the parameter: ```kotlin Storyteller.user.setLocale(null) ``` ## User Customization ### Custom Attributes One of the features of Storyteller is the ability to set custom attributes for requests. These custom attributes can be used to provide additional information about a request, such as the user's location or the device they are using. It can be also used for audience targeting. ### How to Use To set a custom attribute, you can use the `setCustomAttribute()` method of the `Storyteller.user` object. This method takes two parameters: the key of the custom attribute and its value. For example, to set a custom attribute for the user's location, you can use the following code: ```kotlin Storyteller.user.setCustomAttribute('location', 'New York') ``` This will add a custom attribute to all requests made by the user, with the key "location" and the value "New York". You can set multiple custom attributes by calling setCustomAttribute() multiple times with different keys and values. Only String values are allowed. > Setting custom attributes should be done after Storyteller SDK is initialized. To remove custom attribute, you can use the `removeCustomAttribute()` method of the `Storyteller.user` object. This method takes one parameter: the key of the custom attribute to remove. For example, to remove a custom attribute for the user's location, you can use the following code ```kotlin Storyteller.user.removeCustomAttribute("location") ``` ## Followed Categories Storyteller provides a mechanism for managing followed categories associated with clips. ### How to Use #### Set followed categories To set a followed category, call the `addFollowedCategory()` function for a singular category or `addFollowedCategories()` for a collection. Access these methods through the `Storyteller.user` object. For instance, to designate a followed category attribute for the user, implement the following code: ```kotlin Storyteller.user.addFollowedCategory("location") ``` or ```kotlin Storyteller.user.addFollowedCategories(listOf("location", "city", "country")) ``` These categories are stored locally and used to decide whether the plus or tick icon is displayed in the clips UI. > Setting followable categories attributes should be done after Storyteller SDK is initialized. #### Remove followable category To remove a followed category, use the `removeFollowedCategory()` method of the `Storyteller.user` object. This method requires a single parameter—the key of the followed category to be removed. For example, to remove a followable category related to the user's location, utilize the following code: ```kotlin Storyteller.user.removeFollowedCategory("location") ``` This function accepts a singular string parameter, removing the specified category from the locally stored list. ## Storyteller-Managed Following The SDK handles following activity internally and automatically syncs with Storyteller's servers. This simplifies integration by removing the need for custom following logic in your app. This means that all of the above methods on `Storyteller.user` are no-ops in this mode. Also the `categoryFollowActionTaken` is not called. ### Common methods To check if a specific category is being followed, use the `isCategoryFollowed()` method. To retrieve a list of all currently followed categories, use the `getFollowedCategories()` method. ```kotlin val isFollowing: Boolean = Storyteller.user.isCategoryFollowed("location") val followedCategories: Set = Storyteller.user.followedCategories ``` ## AI notes / pitfalls / constraints - externalId should be stable and unique; avoid mutable identifiers. - User IDs are hashed during runtime for VPPA compliance. - Custom attributes and follow categories should be set after initialization. - Re-initializing with a new user resets local page view status. --- # Storyteller Jetpack Compose SDK Source: public-docs/StorytellerLists.md ## Summary - Compose components for StorytellerStoriesRow/Grid and StorytellerClipsRow/Grid. - Data models configure categories or collections, themes, style, and analytics context. - State objects support reloadData for manual refresh. - StorytellerListViewDelegate provides load and tap callbacks and tile info. This is a developers' guide for setting up Storyteller Compose SDK for native Android apps. This guide will cover the basic technical steps for integrating Storyteller Compose SDK into your app. *Note: While this guide focuses on the Jetpack Compose implementation, corresponding traditional Android Views (e.g., `StorytellerStoriesRowView`, `StorytellerStoriesGridView`, `StorytellerClipsRowView`, `StorytellerClipsGridView`) also exist and can be used in XML layouts or programmatically. Their configuration attributes and delegate usage are similar to their Compose counterparts.* ## Getting Started ### Resources - [Storyteller Showcase App (Compose)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/main/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L24) ### Showcase examples - [Lists + Cards feed (`HomeScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/HomeScreen.kt#L164) - [Storyteller list rendering (`StorytellerItem`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/storyteller/StorytellerItem.kt#L58) ### How to Add the SDK to Your Project Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`. #### SDK Installation The Compose SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/). > Note: Starting from 9.3.0 SDK version Compose is now part of the SDK and there is no need to add separate Compose dependency. The steps to add the SDK are in the Getting Started documentation #### Usage 1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc. ```kotlin @Composable fun MainScreen() { Box() { // ... } } ``` 1. Storyteller Composables Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK. ```kotlin StorytellerStoriesRow( modifier = Modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below. delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below. state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below. ) ``` We have the following **composable types** available: - StorytellerStoriesRow - StorytellerStoriesGrid - StorytellerClipsRow - StorytellerClipsGrid 1. Using the Storyteller Composables You can always modify your Composables during recomposition. Reload Data: Just like normal composable lists, they have a list state, so as Storyteller lists, they have a state, and we will use it to reload the data. There is an extra parameter `state` in the Composables, which is set by default. Let's see how it's used: ```kotlin val state = rememberStorytellerRowState() // or rememberStorytellerGridState() for StorytellerStoriesGrid or StorytellerClipsGrid StorytellerStoriesRow( // ... state = state ) // At any point in your code you can call `reloadData` method, maybe after doing PTR or after some event. LaunchedEffect(Unit) { state.reloadData() } ``` 1. Setting categories and collections, applying themes and styles You can use `StorytellerStoriesDataModel` object for configuring your Stories Composables. It has the following properties: ```kotlin StorytellerStoriesDataModel( categories = listOf("category1", "category2"), // list of stories categories for the Composable. If you want to use all categories, you can pass an empty list. theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here. uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app. displayLimit = 6, // a limit of the stories to be displayed in the Composable. Default is Int.MAX_VALUE. cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND. visibleTiles = null, // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size. offset = 1, // the offset to start displaying stories from a specific position in the collection. Default is 0 (start from beginning). context = hashMapOf("placementId" to "home_stories", "location" to "Home"), // Optional analytics context containing integrator-defined key-value pairs. See Analytics for more details. ) ``` > Note: `StorytellerStoriesGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`. You can use `StorytellerClipsDataModel` object for configuring your Clips Composables. It has the following properties: ```kotlin StorytellerClipsDataModel( collection = "collection1", // a string representing your collection of Clips. theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here. uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app. displayLimit = 6, // a limit of the clips to be displayed in the Composable. Default is Int.MAX_VALUE. cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND. visibleTiles = null, // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size. offset = 1, // the offset to start displaying clips from a specific position in the collection. Default is 0 (start from beginning). context = hashMapOf("placementId" to "home_clips", "location" to "Home"), // Optional analytics context containing integrator-defined key-value pairs. See Analytics for more details. ) ``` > Note: `StorytellerClipsGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`. ## Storyteller Compose ListView Delegate The `StorytellerListViewDelegate` will provide you necessary callbacks for the state of data inside you Composables. This delegate is used to track when data load starts, when it completes, when a tile is tapped, and when player was dismissed by user. We recommend using `remember` for the delegate's object to avoid creating new ones during recomposition. ```kotlin val listViewDelegate by remember("your_item_id") { val value = object : StorytellerListViewDelegate { override fun onPlayerDismissed() { // handle player dismissed event } override fun onDataLoadStarted() { // handle loading started event } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int) { // handle data load complete event } override fun onTileTapped(tileType: StorytellerTileType) { // handle tile tap event - called before the player Activity is opened // tileType can be either StorytellerTileType.Story or StorytellerTileType.Clip } } mutableStateOf( value ) } StorytellerStoriesRow( delegate = listViewDelegate, // ... ) ``` ### StorytellerTileType The `StorytellerTileType` is a sealed class that represents the type of tile that was tapped. It contains information about the tapped item and can be one of two types: #### StorytellerTileType.Story Represents a Story tile that was tapped. **Properties:** - `id: String` - The unique identifier of the story - `categories: List` - List of categories that this story belongs to #### StorytellerTileType.Clip Represents a Clip tile that was tapped. **Properties:** - `id: String` - The unique identifier of the clip - `collectionId: String` - The collection ID that this clip belongs to - `categories: List` - List of categories that this clip belongs to **Example usage:** ```kotlin override fun onTileTapped(tileType: StorytellerTileType) { when (tileType) { is StorytellerTileType.Story -> { val storyId = tileType.id val categories = tileType.categories // Handle story tile tap Log.d("TileTap", "Story tapped: $storyId, categories: $categories") } is StorytellerTileType.Clip -> { val clipId = tileType.id val collectionId = tileType.collectionId val categories = tileType.categories // Handle clip tile tap Log.d("TileTap", "Clip tapped: $clipId from collection: $collectionId, categories: $categories") } } } ``` ## StorytellerStoriesGrid The `StorytellerStoriesGrid` is a Composable with same functionality as the `StorytellerStoriesGridView` view from the Storyteller SDK. ### Usage in Compose ```kotlin //Stories Grid StorytellerStoriesGrid( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerGridState(), isEnabled = true ) //Stories Scrollable Grid StorytellerStoriesGrid( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = true, isEnabled = true ) ``` ### Parameters - `modifier` - a `Modifier` object that will be applied to the `StorytellerStoriesGrid` composable. - `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event. - `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state. + the `rememberStorytellerGridState()` has two parameters: - `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid. - `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable. - `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it wil layout all tiles in the grid. - `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled. ## StorytellerStoriesRow The `StorytellerStoriesRow` is a Composable with same functionality as the `StorytellerStoriesRowView` view from the Storyteller SDK. ### Usage in Compose ```kotlin StorytellerStoriesRow( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerRowState() ) ``` ## StorytellerClipsGrid The `StorytellerClipsGridView` is a Composable with same functionality as the `StorytellerClipsGridView` view from the Storyteller SDK. ### Usage in Compose ```kotlin //Clips Grid StorytellerClipsGrid( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = false, isEnabled = true ) //Clips Scrollable Grid StorytellerClipsGrid( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = true, isEnabled = true ) ``` ### Parameters - `modifier` - a `Modifier` object that will be applied to the `StorytellerClipsGrid` composable. - `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event. - `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state. + the `rememberStorytellerGridState()` has two parameters: - `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid. - `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable. - `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it will layout all tiles in the grid. - `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled. - `searchInput` - an optional `SearchInput` object that can be used to filter clips in the grid. - `isVisible` - a `Boolean` flag that indicates if the grid should be visible. Defaults to true. ## StorytellerClipsRow The `StorytellerClipsRow` is a Composable with same functionality as the `StorytellerClipsRowView` view from the Storyteller SDK. ### Usage in Compose ```kotlin StorytellerClipsRow( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerRowState() ) ``` ## AI notes / pitfalls / constraints - Compose SDK is part of the SDK since 9.3.0; no separate dependency. - StorytellerStoriesGrid and StorytellerClipsGrid support only square cell types. - visibleTiles affects row height; null disables visible tile calculations. - Use remember for delegates to avoid recreation on recomposition. ## Related topics - in the Getting Started documentation --- # What is StorytellerListView (Legacy) Source: public-docs/StorytellerListViews.md ## Summary - Legacy XML views for stories and clips rows and grids. - Configure list views with XML attributes or code, including themes and list configuration. - Supports reloadData and dynamic scaling options. - Use StorytellerListViewDelegate for load and tap events. `StorytellerListView` is a base abstract class for views displaying lists of Stories or Clips. Soryteller Views are considered Legacy and the support may be removed in near future. Please prefer using Composables instead. ## Showcase examples - [XML — lists screen (`DashboardFragment`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/DashboardFragment.kt#L78) - [XML — RecyclerView ViewHolders (`ViewHolders`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/adapter/ViewHolders.kt#L38) The Storyteller SDK offers two implementations of this abstract class: - `StorytellerStoriesView` - `StorytellerClipsView` Each of these classes has their corresponding rows and grid views as the following: For Stories - `StorytellerStoriesRowView` - `StorytellerStoriesGridView` For Clips - `StorytellerClipsRowView` - `StorytellerClipsGridView` this abstract class contains all abstract attributes and methods that are common to all `StorytellerListView` implementations. ## How to improve the performance of RecyclerView? If you use the `StorytellerViews` in a `RecyclerView`, using the `setViewHolderParent` method is not mandatory but can significantly improve the UI performance. Inside the `RecyclerView.Adapter`: ```kotlin ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.my_item, parent, false) return MyViewHolder(itemView, parent) } ... ``` Inside the `RecyclerView.ViewHolder`: ```kotlin class MyViewHolder( private val view: View, private val parent: ViewGroup, ): RecyclerView.ViewHolder(view) { private val binding = ListStoryRowBinding.bind(view) fun onBind(item: Item) { binding.storytellerView.setViewHolderParent(parent) ... } } ``` ## Adding a StorytellerStoriesRowView The `StorytellerStoriesRowView` can be added to your app using XML layout or in code. ### XML Add a `com.storyteller.ui.list.StorytellerStoriesRowView` element to your layout ```xml ``` ### Code 1. Create a `StorytellerStoriesRowView` instance ```kotlin val storyRowView = StorytellerStoriesRowView(context) ``` 1. Add the created view instance to the view hierarchy ```kotlin view.addView(storyRowView) ``` ### Recommendations for `StorytellerListView` Hosting Activity Please keep in mind that Stories will open in the activity that displays a status bar only if there is a cutout notch present on the device. In other cases, Stories will open in the activity without a status bar. Status bar visibility changes can have an impact on shared element transitions between Storyteller List View and Story Viewing Activity. For that reason, we recommend hosting activities to use LAYOUT_STABLE flags as in the code snippet below ```kotlin // Storyteller hosting activity onCreate method override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ~~~~~~~ window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // ~~~~~~~ } ``` ## Further Reading ### Additional Storyteller Methods `Storyteller` has some helper methods which may be useful, see AdditionalMethods for more details. ### Implementing Storyteller Callbacks `StorytellerDelegate` has callbacks for events which can be implemented, see StorytellerDelegate for more details. ### Implementing StorytellerListView Callbacks `StorytellerListView` has callbacks for events which can be implemented, see StorytellerListViewDelegate for more details. ## Configuring a StorytellerStoriesView ### Attributes This sections describes attributes common to `StorytellerStoriesRowView` and `StorytellerStoriesGridView`. Notes are used to mark if there is a difference in property interpretation. #### delegate The `delegate` is the `StorytellerListViewDelegate` instance for `StorytellerListView` callbacks (see Implementing StorytellerListViewDelegate methods). #### StorytellerStoriesView.ListConfiguration The `ListConfiguration` class is used to configure the `StorytellerStoriesRowView` and `StorytellerStoriesGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`. Usage: ```kotlin val storytellerStoriesRowView = StorytellerStoriesRowView(context) storytellerStoriesRowView.configuration = StorytellerStoriesView.ListConfiguration( theme = customTheme, uiStyle = StorytellerListViewStyle.AUTO, displayLimit = 10, cellType = StorytellerListViewCellType.SQUARE, categories = listOf("category1", "category2", "categoryX"), offset = 1, context = hashMapOf("placementId" to "home_stories", "location" to "Home"), ) ``` ##### cellType The `cellType` is the style of the cell. This can either be `ROUND(0)` or `SQUARE(1)`. The default value is `SQUARE(1)`. ##### categories The `categories` attribute is a list of Categories that the list view should show to the user. The default value is an empty list. The `categories` property can be used to show specific Stories content in the row or grid by supplying a list of valid Categories as strings. Categories can be defined in the CMS. If no Categories are assigned then the content from the default or "Home" Category is displayed. ##### theme The `theme` parameter is used to set the Theme for the `StorytellerListView`. If theme is set to null, then theme set as`Storyteller.theme` global property is used. The theme determines how the items within the List View are presented as well as various features of the player once it is launched. ##### uiStyle The `uiStyle` adjusts whether Storyteller renders in light mode, dark mode or follows the system setting. `uiStyle` takes the following values: - `StorytellerListViewStyle.AUTO` - default value, the StorytellerListView will adjust its color scheme automatically according to the current system UI mode - `StorytellerListViewStyle.LIGHT` - force the StorytellerListView to use the light theme - `StorytellerListViewStyle.DARK` - force the StorytellerListView to use the dark theme ##### displayLimit The `displayLimit` is the maximum amount of tiles that can be shown in the list. ##### offset The `offset` is the offset to start displaying stories from a specific position in the collection. Default is 0 (start from beginning). ##### context The `context` parameter is an optional `StorytellerAnalyticsContext` (which is a `HashMap`) containing integrator-defined key-value pairs for analytics attribution. This context will be included with all analytics events originating from this UI instance. See Analytics for more details. #### Dynamic Scaling Item tiles will scale dynamically to fit the row or grid view. The base size of the tiles are: - 100dp width x 150dp height for square tiles - 76dp width x 105dp height for round tiles The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents. In general, tiles will maintain its base proportion and scale up or down to meet view hierarchy constrains. For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations. ##### Example 1 ```xml ``` The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp` ##### Example 2 ```xml ``` The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerStoriesRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly. ##### Example 3 ```xml ``` In this case, the final item tile size will be the height of the window. This happens because the `StorytellerStoriesRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also. ##### Example 4 ```xml ... ... ``` In this case, the final item tile size will be 100dp x 150dp (the base size). Since the `StorytellerStoriesRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size 100dp x 150dp (base size). #### XML Attributes can also be applied in XML. ```xml ``` ### Methods #### reloadData The `reloadData` method starts loading fresh data for all Stories from the API. On completion, it updates the Story data, starts prefetching content and updates the read status of the Stories. The `onDataLoadStarted` and `onDataLoadComplete` methods on the StorytellerListViewDelegate are called accordingly (the latter with appropriate data depending on the result of the API requests). ```kotlin val storytellerStoriesRowView = StorytellerStoriesRowView(context) storytellerStoriesRowView.reloadData() ``` ## Configuring a StorytellerClipsView ### Attributes This sections describes attributes common to `StorytellerClipsRowView` and `StorytellerClipsGridView`. Notes are used to mark if there is a difference in property interpretation #### StorytellerListViewDelegate - `delegate`: the `StorytellerListViewDelegate` instance for `StorytellerClipsView` callbacks (see Implementing StorytellerListViewDelegate methods) #### StorytellerClipsView.ListConfiguration The `ListConfiguration` class is used to configure the `StorytellerClipsRowView` and `StorytellerClipsGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`. `StorytellerClipsView.ListConfiguration` class attributes: Example: ```kotlin val storytellerClipsRowView = StorytellerClipRowView(context) storytellerClipsRowView.configuration = StorytellerClipsView.ListConfiguration( theme = customTheme, uiStyle = StorytellerListViewStyle.AUTO, displayLimit = 10, cellType = StorytellerListViewCellType.SQUARE, collection = "collectionId", context = hashMapOf("placementId" to "home_clips", "location" to "Home"), ) ``` - `collection`: the Collection that the list view should show to the user. The default value is `null`.The `collection` property is used to show specific Clips content in the row or grid by supplying a single Collection as string. Collections can be defined in the CMS. If no Collection is assigned, then no Clips content is displayed. - `theme`: This parameter is used to set the Theme for the `StorytellerClipsView`. If theme is set to `null`, then theme set as`Storyteller.theme` global property is used. The theme determines how the items within the List View are presented as well as various features of the player once it is launched. - `displayLimit`: only display up to this number of tiles in the list. - `uiStyle`: adjust whether Storyteller renders in light mode, dark mode or follows the system setting. `uiStyle` takes the following values: - `StorytellerListViewStyle.AUTO` - default value, the StorytellerClipsView will adjust its color scheme automatically according to the current system UI mode - `StorytellerListViewStyle.LIGHT` - force the StorytellerClipsView to use the light theme - `StorytellerListViewStyle.DARK` - force the StorytellerClipsView to use the dark theme - `context`: Optional `StorytellerAnalyticsContext` (a `HashMap`) containing integrator-defined key-value pairs for analytics attribution. This context will be included with all analytics events originating from this UI instance. See Analytics for more details. #### Feed Title The feed title in the Clips player can be configured in the CMS for the Collection. This can be a custom title or image. ### Dynamic Scaling Item tiles will scale dynamically to fit the row or grid view. The base size of the tiles are: - 100dp width x 150dp height for square tiles - 76dp width x 105dp height for round tiles The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents. In general, tiles will maintain its base proportions and scale up or down to meet view hierarchy constrains. For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations. #### Example 1 ```xml ``` The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp` #### Example 2 ```xml ``` The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly. #### Example 3 ```xml ``` In this case, the final item tile size will be the height of the window. This happens because the `StorytellerRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also. #### Example 4 ```xml ... ... ``` In this case, the final item tile size will be `100dp x 150dp` (the base size). Since the `StorytellerRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size `100dp x 150dp` (base size). ### XML Views can also be applied in XML. ```xml ``` ### Methods #### reloadData The `reloadData` method starts loading fresh data for all Clips from the API. On completion, it updates the Clips data, starts prefetching content and updates the read status of the Clips. The `onDataLoadStarted` and `onDataLoadComplete` methods on the StorytellerListViewDelegate are called accordingly (the latter with appropriate data depending on the result of the API requests). ```kotlin val storytellerClipsRowView = StorytellerClipsRowView(context) storytellerClipsRowView.reloadData() ``` ## AI notes / pitfalls / constraints - Legacy views are supported but Compose is preferred for new work. - Dynamic scaling requires layout constraints; follow the doc examples. - reloadData triggers a network fetch and updates the view. ## Related topics - Composables instead - AdditionalMethods - StorytellerDelegate - StorytellerListViewDelegate - Implementing StorytellerListViewDelegate methods - Theme - Analytics --- # StorytellerClipsFragment Source: public-docs/StorytellerClipsFragment.md ## Summary - Embed clips using StorytellerClipsFragment via XML or programmatically. - Configure collection ID and optional initialCategory. - Control playback with shouldPlay and listener callbacks. - reloadData refreshes content; Compose integration is supported. `StorytellerClipsFragment` is a fragment that can be used to embed a clip in your own activities. Like a regular Android Fragment, it can be configured from an XML layout or instantiated programmatically and attached to the host's (another fragment or activity) FragmentManager via a fragment transaction. > Clips are delivered in a 9:16 aspect ratio. While `StorytellerClipsFragment` will do content > cropping in a way that all given space is taken by the content (all area is consumed, cropping is > applied to avoid content distortion). If you want to present content without > cropping, `StorytellerClipsFragment` should reside > in a container that maintains a 9:16 aspect ratio. ## Showcase examples - [Compose — embedded clips (`MomentsScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/moments/MomentsScreen.kt#L160) - [XML — embedded clips (`EmbeddedClipFragment`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/embedded/EmbeddedClipFragment.kt#L74) ## Using StorytellerClipsFragment from xml layout `StorytellerClipsFragment`can be used directly from the layout XML files. The usage is identical to typical Android fragments. You need to specify the fully qualified fragment class name in the android:name property and the collection ID using app:storyteller_collection_id_property. Note that in this example, a constraint is applied to the dimensions' proportions so the clip content is not cropped at all. app:storyteller_initial_category is optional and can be used to set the initial category of the collection to be viewed. If this is used. The clip will be first loaded with the collection. The selected category will then be navigated to automatically. ```xml ``` ## Using StorytellerClipsFragment programmatically `StorytellerClipsFragment` can also be used programmatically. To embed a clip fragment programmatically, you need to follow these steps: 1. Create a new fragment instance using the `StorytellerClipsFragment.create(collectionId: String, context: StorytellerAnalyticsContext)` method. a. Optionally, you can also set the `initialCategory` property to a string value to automatically navigate to a selected category. 2. Create a fragment transaction that would add this fragment to the fragment container view. 3. Commit the created transaction. See the following snippet illustrating attaching a fragment in the `Activity.onCreate` method: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // An example using viewBinding feature; see view binding for reference. val binding = ActivityClipFragmentHostBinding.inflate(layoutInflater) setContentView(binding.root) val storytellerClipsFragment = StorytellerClipsFragment.create( collectionId = "yourCollectionId", context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val transaction = supportFragmentManager.beginTransaction() transaction.add( binding.fragmentHost.id, storytellerClipsFragment ) transaction.commit() } ``` ## Controlling playback in StorytellerClipsFragment `StorytellerClipsFragment` by default starts playback as soon as it is attached to the host and visible. Standard Android Fragment lifecycle events typically handle automatic pausing and resuming (e.g., when the app goes to the background), complementing the manual control provided by the `shouldPlay` property. ```kotlin // finding the fragment by id using the fragment manager val storytellerClipsFragment = supportFragmentManager.findFragmentById(binding.fragmentHost.id) as StorytellerClipsFragment storytellerClipsFragment.shouldPlay = false // stops playback storytellerClipsFragment.shouldPlay = true // starts playback ``` `StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val canGoBack = storytellerClipsFragment.canGoBack ``` `StorytellerClipsFragment` also contains `listener` property which can be used to control playback and handle top level back button press. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped / host should handle back } override fun onDataLoadStarted() { // will be invoked when data load starts } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){ // will be invoked when data load completes or fails } } storytellerClipsFragment.reloadData() ``` ## External back button handling `StorytellerClipsFragment` does not show the top level back button by default. You can control the behaviour by setting `topLevelBackEnabled` property and setting `onTopLevelBackPressed` according to your needs. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped } } ``` `StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val canGoBack = storytellerClipsFragment.canGoBack ``` ## Initial Category To set the `StorytellerClipsFragment` to start with a specific category, you can set the `initialCategory` property. Programmatical Usage ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", initialCategory = "yourCategory", context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) ``` XML Usage ```xml ``` If the category is found in the collection, the clip will be loaded with the collection and the selected category. The category will be navigated to automatically. If the category is not found in the collection or is invalid, it will be ignored. ## Reload Data `StorytellerClipsFragment` in order to reload data you can call `reloadData()` method. This method will make a request to the backend to fetch the latest data. If this method is called when there are category filters applied, then it will go back one level. If there are no category filters applied this method will reload data. Loading state can be observed by setting `listener` property and overriding it's `onDataLoadStart` and `onDataLoadComplete` methods. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = hashMapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped / host should handle back } override fun onDataLoadStarted() { // will be invoked when data load starts } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){ // will be invoked when data load completes or fails } } storytellerClipsFragment.reloadData() ``` ## Inset Management One of the usecase of `StorytellerClipsFragment` is to embed it in a view that is laid behind status bar. While that will increase size of the screen available for the content, this arrangement can potentially causing the clip title and other UI elements to be obscured by the status bar. To avoid this problem you can set `StorytellerClipsFragment.topInset` property to the height of the status bar. This will cause the fragment to offset the title and the top buttons by the given amount of pixels. Similarly if the bottom part of the view is obscured by the Navbar, you can set `StorytellerClipsFragment.bottomInset` property to the height of the navigation bar. The recommended approach is to use this property together with `onApplyWindowInsets` callback. ```kotlin ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, winInsets -> val inset = winInsets.getInsets(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) storytellerClipsFragment.topInset = inset.top storytellerClipsFragment.bottomInset = inset.bottom WindowInsetsCompat.CONSUMED } ``` ## Compose Integration `StorytellerClipsFragment` can be used in Jetpack Compose using the `StorytellerEmbeddedClips` composable. ```kotlin class DemoComposeEmbeddedClipsActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val collection = intent.getStringExtra("collection") ?: "" val widthPercentage = intent.getIntExtra("width", 100) / 100F val heightPercentage = intent.getIntExtra("height", 80) / 100F val topLevelBack = intent.getBooleanExtra("topLevelBack", false) val initialCategory = intent.getStringExtra("category") ?: "" enableEdgeToEdge() setContent { val state = rememberStorytellerEmbeddedClipsState( collectionId = collection, topLevelBack = topLevelBack, initialCategory = initialCategory, context = hashMapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) StorytellerEmbeddedClips( modifier = Modifier .fillMaxWidth(widthPercentage) .fillMaxHeight(heightPercentage), state = state, ) } } companion object { fun start(context: Context, collection: String, category: String?, width: Int, height: Int, topLevelBack: Boolean) { // add params to intent Intent(context, DemoComposeEmbeddedClipsActivity::class.java).apply { putExtra("collection", collection) putExtra("category", intialCategory) putExtra("width", width) putExtra("height", height) putExtra("topLevelBack", topLevelBack) context.startActivity(this) } } } } ``` ### StorytellerEmbeddedClipsState `rememberStorytellerEmbeddedClipsState` is a composable function that creates a `StorytellerEmbeddedClipsState` object that holds the state of the `StorytellerEmbeddedClips` composable. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = "collection", topLevelBack = topLevelBack, context = hashMapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) StorytellerEmbeddedClips( modifier = Modifier, state = state ) ``` `StorytellerEmbeddedClipsState` contains `canGoBack` property that can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = "collection", topLevelBack = topLevelBack, context = hashMapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) val canGoBack = state.canGoBack // true if user can navigate back ``` `StorytellerEmbeddedClipsState` contains `goBack()` which will move the content to previous Category if the user is not at the top level. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = collection, topLevelBack = topLevelBack, context = hashMapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) state.goBack() // navigate to previous category programmatically ``` ## AI notes / pitfalls / constraints - Clips are 9:16; use a 9:16 container to avoid cropping. - initialCategory is ignored if invalid or not in the collection. - topLevelBackEnabled controls top-level back handling and is false by default. - reloadData can reset category filters; observe load callbacks via listener. --- # Storyteller Home Source: public-docs/StorytellerHome.md ## Summary - StorytellerHome embeds multiple rows and grids on a single screen. - Available as a Compose component or StorytellerHomeFragment. - reloadData refreshes content via state or fragment method. - theme and uiStyle default to global theme and AUTO. `StorytellerHome` is a component which allows multiple Stories and Clips Rows or Grids to be embedded in a single screen in your application in a list. ## How to Use The code samples below show how to use the `StorytellerHome` component in both `Compose` and `Fragment`. It includes a pull-to-refresh (PTR) mechanism, but you can also use the `reloadData` method to refresh data on your side. *Defaults: If `theme` is not provided, the global theme set via `Storyteller.theme` will be used. If `uiStyle` is not provided, it defaults to `StorytellerListViewStyle.AUTO`, which follows the system's light/dark mode setting.* `context` - optional context data that will be included in analytics callbacks for attribution. This allows you to track which sources drive engagement with your home content. See Analytics for more details. ### Compose ```kotlin // without reload data outside sdk StorytellerHome( homeId = "home", theme = yourTheme, uiStyle = StorytellerListViewStyle.AUTO, context = hashMapOf("placementId" to "home_screen", "location" to "Home"), modifier = Modifier ) // with reload data outside sdk val state = rememberStorytellerHomeState() StorytellerHome ( homeId = "home", theme = yourTheme, uiStyle = StorytellerListViewStyle.AUTO, context = hashMapOf("placementId" to "home_screen", "location" to "Home"), // Optional analytics context state = state, modifier = Modifier ) // ... // call for reload data outside sdk suspend fun reloadData() { state.reloadData() } ``` ### Fragment ```kotlin val fragment = StorytellerHomeFragment.create( homeId = "home", context = hashMapOf("placementId" to "home_screen", "location" to "Home"), // Optional analytics context // theme = yourTheme, // Optional: Defaults to Storyteller.theme // uiStyle = StorytellerListViewStyle.AUTO, // Optional: Defaults to AUTO ) // call for reload data outside sdk fragment.reloadData() ``` ## AI notes / pitfalls / constraints - homeId is required to load the home configuration. - context is optional and flows into analytics callbacks. - Use rememberStorytellerHomeState when you need external reload control. ## Related topics - Analytics --- # Custom Themes Source: public-docs/Themes.md ## Summary - Customize UI via UiTheme, set globally or per list view. - Theme covers colors, fonts, primitives, list layout, tiles, player, buttons, and other components. - UiTheme DSL supports builder patterns and light or dark variants. - Theme builder supports scoped overrides for colors, fonts, and drawables. The appearance of the SDK can be customized by setting the `Storyteller.theme` global property or `StorytellerListView.theme` view property. This requires a `UiTheme` configuration object. > Note: global theme should be set before any of the Storyteller list views are inflated. > Note: this property will be used as fallback theming style used to render Story items in lists and activities launched from list. > See StorytellerLists or StorytellerListViews (legacy). ## Showcase examples - [Compose — theme building (`GetConfigurationUseCase`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/domain/GetConfigurationUseCase.kt#L60) - [XML — theme selection (`GetHomeScreenUseCase`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/domain/GetHomeScreenUseCase.kt#L21) UiTheme is a data structure containing tree of the parameters. While it's possible to manually declare a whole structure of this data object, for convenience it's recommended to use UiTheme DSL for that purpose. ## Defining a Theme A `Theme` contains various settings which change visual aspects of the Storyteller SDK. All properties are optional. In general, it should be possible to obtain a custom look by only using the `colors` properties - however, if you require more fine-grained control, that is also available. ### Colors The `colors` property on theme is used to establish a set of base colors for the SDK to use. | Colors | Description | Default Value | Dark Value | | :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | :--------- | | `theme.colors.primary` | Default accent color used throughout the UI. Usually the primary brand color. Used for: Unread Indicators, loading progress bars and spinners on Android. | #1C62EB | | | `theme.colors.success` | Used to indicate correct answers in Quizzes. | #3BB327 | | | `theme.colors.alert` | Used to indicate wrong answers in Quizzes and for the 'Live' indicator on Live Stories. | #E21219 | | | `theme.colors.white.primary` | Used for Story names on rectangular tiles and in the player. Also used for all primary text in dark mode by default. | #ffffff | | | `theme.colors.white.secondary` | Used for secondary text in dark mode. | rgba({theme.colors.white.primary}, 85%) | | | `theme.colors.white.tertiary` | Used for tertiary text in dark mode, e.g. the time stamp in the player header. Also used for the selected Poll option border. | rgba({theme.colors.white.primary}, 70%) | | | `theme.colors.black.primary` | Used for primary text in light mode by default. | #1A1A1A | | | `theme.colors.black.secondary` | Used for secondary text in light mode. | rgba({theme.colors.black.primary}, 85%) | | | `theme.colors.black.tertiary` | Used for tertiary text and minor UI elements in light mode. | rgba({theme.colors.black.primary}, 70%) | | ### Font Use the `font` property to set a custom font for use throughout the UI. | Font | Description | Default Value | Dark Value | | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ | :--------- | | `theme.font` | Font to be used throughout the UI, defaults to the system font on each platform or inherits the container font on web. Font definition requires access to different weights to work properly. | System Font | | ### Primitives The `primitives` object contains base values which are used throughout the UI. | Primitives | Description | Default Value | Dark Value | | :------------------------------ | :------------------------------------------------------------------------------- | :------------ | :--------- | | `theme.primitives.cornerRadius` | Default corner radius used for Rectangular Tiles, Buttons and Poll/Quiz answers. | 8 dp | | ### Lists The `lists` customize properties of the various list types available from the SDK. | Lists | Description | Default Value | Dark Value | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------- | :--------------------------- | | `theme.lists.title` | The style of the title on Story or Clip tile. | 8 dp | | | `theme.lists.row.tileSpacing` | The space between each Story and Clip Tile in a row. | 8 dp | | | `theme.lists.row.startInset` | The space before the first tile in a row. | 12 dp | | | `theme.lists.row.endInset` | The space after the last tile in a row. | 12 dp | | | `theme.lists.grid.tileSpacing` | The space between Story and Clip Tiles in a grid, both vertically and horizontally. | 8 dp | | | `theme.lists.grid.columns` | Number of columns in the grid. | 2 dp | | | `theme.lists.grid.topInset` | The space at the top of the first row in a grid. | 0 dp | | | `theme.lists.grid.bottomInset` | The space at the bottom of the last row in a grid. | 0 dp | | | `theme.lists.backgroundColor` | Required for outline on Live chip and fade to the side of the row | {theme.colors.white.primary} | {theme.colors.black.primary} | | `theme.lists.animateTilesOnReorder` | This option allows you to enable animation in lists when updating items | true | true | | `theme.lists.enablePlayerOpen` | Controls whether the SDK opens the player when a tile is tapped. When set to `false`, the SDK will not open the player and the app must handle tile taps via `StorytellerDelegates.onTileTapped(tileType: StorytellerTileType)` | true | true | ### Gradient The `Gradient` data class allows for the creation of a color gradient, with options to customize both the colors and the positions at which the gradient starts and ends. | Property | Default Value | Data Type | Description | | --------------- | ------------- | ------------------ | -------------------------------------------------- | | `startColor` | `null` | `ColorInt` | The color where the gradient begins. | | `endColor` | `null` | `ColorInt` | The color where the gradient ends. | | `startPosition` | `null` | `GradientPosition` | The position indicating where the gradient starts. | | `endPosition` | `null` | `GradientPosition` | The position indicating where the gradient ends. | #### Enum: GradientPosition Defines positions for starting and ending points of the gradient. | Value | Data Type | Description | | ------------------------------- | --------- | ----------------------------------------- | | `GradientPosition.BottomLeft` | `enum` | Bottom left corner of the gradient area. | | `GradientPosition.BottomCenter` | `enum` | Bottom center edge of the gradient area. | | `GradientPosition.BottomRight` | `enum` | Bottom right corner of the gradient area. | | `GradientPosition.CenterLeft` | `enum` | Center left edge of the gradient area. | | `GradientPosition.CenterCenter` | `enum` | Center of the gradient area. | | `GradientPosition.CenterRight` | `enum` | Center right edge of the gradient area. | | `GradientPosition.TopLeft` | `enum` | Top left corner of the gradient area. | | `GradientPosition.TopCenter` | `enum` | Top center edge of the gradient area. | | `GradientPosition.TopRight` | `enum` | Top right corner of the gradient area. | ### Story and Clip Tiles The `tiles property` can be used to customize the appearance of the Story and Clip Tiles. | Story and Clip Tiles | Description | Default Value | Dark Value | | :-------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------ | :---------------------------- | | `theme.tiles.chip.textSize` | Text size for the New Indicator and Live Indicator. | 11 sp | | | `theme.tiles.chip.borderColor` | Border color for the New Indicator and Live Indicator. | null | | | `theme.tiles.chip.show` | Show or hide the New/Live chip indicator on tiles. | true | | | `theme.tiles.title.textSize` | Size of the Story Title on a Tile. | 11 sp | | | `theme.tiles.title.lineHeight` | Line height of the Story Title on a Tile. | 13 sp | | | `theme.tiles.title.alignment` | Alignment of the text in a tile, can be Gravity.START/CENTER/END | center | | | `theme.tiles.circularTile.title.unreadTextColor` | Text color of Circular Story and Clip Tile Titles in unread state | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.tiles.circularTile.title.readTextColor` | Text color of Circular Story and Clip Tile Titles in read state | {theme.colors.black.tertiary} | {theme.colors.white.tertiary} | | `theme.tiles.circularTile.unreadIndicatorColor` | The color of the ring around Circular Story and Clip Tiles. | {theme.colors.primary} | | | `theme.tiles.circularTile.readIndicatorColor` | The color of the ring around Circular Story and Clip Tiles in read state | #C5C5C5 (taken from app layout) | | | `theme.tiles.circularTile.unreadIndicatorBorderColor` | The color of the border of the ring around Circular Story and Clip Tiles | null | | | `theme.tiles.circularTile.readIndicatorBorderColor` | The color of the border of the ring around Circular Story and Clip Tiles in read state | null | | | `theme.tiles.circularTile.unreadBorderWidth` | Width of Circular Story and Clip Tile ring border in unread state | 2dp | | | `theme.tiles.circularTile.readBorderWidth` | Width of Circular Story and Clip Tile ring border in read state | 2dp | | | `theme.tiles.circularTile.unreadIndicatorGradient` | The gradient of the ring around a circular tile when the story or the clip is unread. If set, overrides `circularTile.unreadIndicatorColor`. | null | | | `theme.tiles.circularTile.liveChip.unreadImage` | Image resource to be used in place of default unread Live Indicator. If set, overrides `theme.tiles.circularTile.liveChip.unreadBackgroundGradient`. | null | | | `theme.tiles.circularTile.liveChip.unreadBackgroundGradient` | The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides `circularTile.liveChip.unreadBackgroundColor`. | null | | | `theme.tiles.circularTile.liveChip.unreadBackgroundColor` | Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.alert} | | | `theme.tiles.circularTile.liveChip.unreadBorderColor` | Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | null | | | `theme.tiles.circularTile.liveChip.unreadTextColor` | Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.white.primary} | | | `theme.tiles.circularTile.liveChip.readImage` | Image resource to be used in place of default read Live Indicator. | null | | | `theme.tiles.circularTile.liveChip.readBackgroundColor` | Background color of the Live Indicator when all pages have been read or the clip has been viewed. | {theme.colors.black.tertiary} | | | `theme.tiles.circularTile.liveChip.readBorderColor` | Border color of the Live Indicator when all pages have been read or the clip has been viewed. | null | | | `theme.tiles.circularTile.liveChip.readTextColor` | Text color of the Live Indicator when all story pages have been read or the clip has been viewed. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.title.textColor` | Text color of the Story Title in Rectangular Tiles. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.chip.alignment` | Alignment of the New Indicator and Live Indicator in Rectangular Tiles, can be start, center or end. | end | | | `theme.tiles.rectangularTile.padding` | Internal padding for Rectangular Story and Clip Tiles, creates space between Story Name or New Indicator and tile edge. | 8 dp | | | `theme.tiles.rectangularTile.unreadIndicator.image` | Image resource to be used in place of default New Indicator on Rectangular Tiles. | null | | | `theme.tiles.rectangularTile.unreadIndicator.backgroundColor` | Background color of the New Indicator. | {theme.colors.primary} | | | `theme.tiles.rectangularTile.unreadIndicator.borderColor` | Border color of the New Indicator. | null | | | `theme.tiles.rectangularTile.unreadIndicator.textColor` | The text color of the unread indicator for a rectangular tile. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.unreadIndicator.textSize` | Text size for the New Indicator. | 11 sp | | | `theme.tiles.rectangularTile.liveChip.unreadImage` | Image resource to be used in place of default unread Live Indicator. If set, overrides `theme.tiles.circularTile.liveChip.unreadBackgroundGradient`. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBackgroundGradient` | The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides `circularTile.liveChip.unreadBackgroundColor`. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBackgroundColor` | Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.alert} | | | `theme.tiles.rectangularTile.liveChip.unreadTextColor` | Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.liveChip.readImage` | Image resource to be used in place of default read Live Indicator. | null | | | `theme.tiles.rectangularTile.liveChip.readBackgroundColor` | Background color of the Live Indicator when all pages have been read or the clip has been viewed. | {theme.colors.black.tertiary} | | | `theme.tiles.rectangularTile.liveChip.readBorderColor` | Border color of the Live Indicator when all pages have been read or the clip has been viewed. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBorderColor` | Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | null | | | `theme.tiles.rectangularTile.liveChip.readTextColor` | Text color of the Live Indicator when all story pages have been read or the clip has been viewed. | {theme.colors.white.primary} | | ### Player The `player` property is used to customize properties relating to the Stories and Clips Player. | Player | Description | Default Value | Dark Value | | :----------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | :--------- | | `theme.player.showStoryIcon` | Shows the circular Story icon before the Story Name in the Player. | FALSE | | | `theme.player.showTimestamp` | Shows a timestamp after the Story Name in the Player, eg "2h" to show the Story was published 2 hours ago. | TRUE | | | `theme.player.showShareButton` | Shows the Share button in the Player, applies to all Page types and Engagement Units. Setting to FALSE entirely disables sharing in Storyteller. | TRUE | | | `theme.player.liveChip.image` | Image used in place of Live Chip before Live Story or Clip Titles. If set, it overrides `liveChip.backgroundGradient` | null | | | `theme.player.liveChip.backgroundGradient` | Background gradient of the badge for Live Story or Clip. If set, it overrides `liveChip.backgroundColor` | null | | | `theme.player.liveChip.backgroundColor` | Background color of the Live chip Story or Clip | {theme.colors.alert} | | | `theme.player.liveChip.textColor` | Text color used for badge label for Live Story or Clip | {theme.colors.white.primary} | | | `theme.player.liveChip.borderColor` | Color used for border of badge label for Live Story or Clip | null | | | `theme.player.icons.share` | Share button image to be used in place of default share icon. | default icon | | | `theme.player.icons.refresh` | Refresh button image to be used in place of default refresh icon. | default icon | | | `theme.player.icons.close` | Close button image to be used in place of default close icon. | default icon | | | `theme.player.icons.like.initial` | Initial like button image to be used in place of default initial like icon. | default icon | | | `theme.player.icons.like.liked` | Liked button image to be used in place of default liked icon. | default icon | | | `theme.player.showLikeButton` | Shows the Like button on Clips. | TRUE | | ### Buttons The `buttons` property applies customizations to buttons which appear throughout the UI. | Buttons | Description | Default Value | Dark Value | | :------------------------------ |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| :------------------------------ | :--------- | | `theme.buttons.backgroundColor` | Background color of buttons including: share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. | {theme.colors.white.primary} | | | `theme.buttons.textColor` | Text color of buttons including: share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. | {theme.colors.black.primary} | | | `theme.buttons.textCase` | Sets the text case for the button on the Instructions Screen, share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state (TextCase.UPPER/DEFAULT/LOWER). | default | | | `theme.buttons.cornerRadius` | Sets the corner radius for buttons including: Instructions Screen button, share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. Any value greater than half the height of the button will create a pill shape. | {theme.primitives.cornerRadius} | | ### Instructions Use the `instructions` property to customize the appearance of the instructions screen. | Instructions | Description | Default Value | Dark Value | | :------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | :----------------------------- | | `theme.instructions.show` | Show the Instructions Screen the first time a user opens Storyteller. Set to FALSE to entirely hide the Instructions screen. | TRUE | | | `theme.instructions.headingColor` | Heading color of the text used on the Instructions Screen. | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.instructions.headingTextCase` | Sets the text case for the heading on the Instructions Screen. (TextCase.UPPER/DEFAULT/LOWER). | TextCaseTheme.DEFAULT | | | `theme.instructions.headingFont` | Font to be used on the Instuctions Screen for headers, defaults to the system font | System font | | | `theme.instructions.subHeadingColor` | Subheading color of the text used on the Instructions Screen. | {theme.colors.black.secondary} | {theme.colors.white.secondary} | | `theme.instructions.backgroundColor` | Background color of the Instructions Screen. | {theme.colors.white.primary} | {theme.colors.black.primary} | | `theme.instructions.icons` | A set of icons used for each instruction on the Instructions Screen | default set of icons | | | `theme.instructions.button.backgroundColor` | Background color of the button used on the Instructions Screen. | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.instructions.button.textColor` | Text color of the button used on the Instructions Screen. | {theme.colors.white.primary} | {theme.colors.black.primary} | ### Engagement Units The `engagementUnits` property can be used to customize properties relating to Polls and Quizzes. | Engagement Units | Description | Default Value | Dark Value | | :----------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | :--------- | | `theme.engagementUnits.poll.answerTextColor` | Answer text color used in Poll Answers. | {theme.colors.black.primary} | | | `theme.engagementUnits.poll.percentBarColor` | Background color of the Percentage Bar in Poll Answers. | #CDD0DC | | | `theme.engagementUnits.poll.selectedAnswerBorderColor` | Border color added to the selected Poll Answer. Inherits `colors.primary` | {theme.colors.primary} | | | `theme.engagementUnits.poll.answeredMessageTextColor` | Color of the vote count or "Thanks for Voting!" message shown to users. | {theme.colors.white.primary} | | | `theme.engagementUnits.poll.selectedAnswerBorderImage` | Border image used for the selected Poll Answer. Overwrites selectedAnswerBorderColor and can be used to create a shimmer animation as the border image is rotated in when an answer is selected. | null | | | `theme.engagementUnits.poll.showPercentBarBackground` | Adds a striped background under the percentage bar in Poll Answers. | FALSE | | | `theme.engagementUnits.triviaQuiz.correctColor` | Color used for correct answers in Trivia Quizzes. | {theme.colors.success} | | | `theme.engagementUnits.triviaQuiz.incorrectColor` | Color used for incorrect answers in Trivia Quizzes. | {theme.colors.alert} | | ### Search The `search` property applies customizations to the `Search` component. | Title | Description | Default Value | Dark Value | | ----------------------- | ------------------------------------------------- | ------------------- | ------------------- | | `theme.search.heading` | Styling of the `Filters` title in the Filter View | `theme.lists.title` | `theme.lists.title` | | `theme.search.backIcon` | Image to be used as a back icon in the Search UI | default icon | | ### Home | Title | Description | Default Value | Dark Value | | ---------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ---------- | | `theme.home.headerTitle.font` | Font to be used throughout the UI, defaults to the system font | System font | | | `theme.home.headerTitle.textSize` | Size of the Home Header Title | 22sp | | | `theme.home.headerTitle.lineHeight` | Line height of the Home Header Title | 25sp | | | `theme.home.headerTitle.textCase` | Sets the text case for the Home Header Title. (TextCase.UPPER/DEFAULT/LOWER). | TextCaseTheme.DEFAULT | | | `theme.home.headerTitle.textColor` | Text color of the Home Title | colors.black.primary | | | `theme.home.circularTitle.textSize` | The size of the title for Circular tiles | lists.title | | | `theme.home.circularTitle.lineHeight` | The line height of the title for Circular tiles | lists.title | | | `theme.home.gridTitle.textSize` | The size of the title for tiles in Grid | 16sp | | | `theme.home.gridTitle.lineHeight` | The line height of the title for Grid tiles | 22sp | | | `theme.home.singletonTitle.textSize` | The size of the title for Singleton tile | 22sp | | | `theme.home.singletonTitle.lineHeight` | The line height of the title for Singleton tiles | 28sp | | | `theme.home.rectangularTitle.smallTitle.textSize` | The size of the title for small Rectangular row tiles | lists.title | | | `theme.home.rectangularTitle.smallTitle.lineHeight` | The title line height for the small Rectangular row tiles | lists.title | | | `theme.home.rectangularTitle.mediumTitle.textSize` | The size of the title for medium Rectangular row tiles | 16sp | | | `theme.home.rectangularTitle.mediumTitle.lineHeight` | The title line height for the medium Rectangular row tiles | 22sp | | | `theme.home.rectangularTitle.largeTitle.textSize` | The size of the title for large Rectangular row tiles | 18sp | | | `theme.home.rectangularTitle.largeTitle.lineHeight` | The title line height for the large Rectangular row tiles | 24sp | | ### Theme Builder Initialization Theme builder is initialized by the `buildTheme` method lambda passed after the method acts in the builder scope. ```kotlin import com.storyteller.domain.theme.builders.buildTheme import com.storyteller.domain.theme.builders.ofHexCode Storyteller.theme = buildTheme { light.colors.primary = ofHexCode("#FF00FF") } ``` ### Accessing Properties in the Builder Scope There are 2 equivalent ways of accessing builder properties: - By scopes ```kotlin buildTheme { light { instructions { button { backgroundColor = ofHexCode("#00FF00") } } } } ``` - By properties ```kotlin buildTheme { light.instructions.button.backgroundColor = ofHexCode("#00FF00") } ``` Both approaches produce the same effect. ### Light and Dark Builder Variants In the builder context two variants are present: - light - dark Although they have an identical structure, they are build with the different set of fallbacks. For instance, default `theme.tiles.circularTile.title.unreadTextColor` will fallback to the default value of `theme.colors.black.primary` in the light mode or `theme.colors.white.primary` in the dark mode. The selection of active themes will be done using current phone UI mode and `StorytellerListView.uiStyle` property value. See StorytellerListViews for more details. For coding convenience, if you do not intent use light and dark mode and relay on default fallback you can use `from` inline method to copy already set values from one theme to the other. ```kotlin buildTheme { light { colors { primary = ofHexCode("#FF00FF") success = ofHexCode("#00FF00") } } dark from light } ``` The above code will set all properties of dark to the current state of the light builder. This method is useful to avoid lengthy typing - a common parameter can be assigned one and copied to the other variant. ### Setting Properties of Particular Type #### Colors Color properties are expected to be Android @ColorInt. They can be initialized with anything that return such type e.g they can be resolved color from resources, Color.argb() Color.BLUE and so on. For convenience, `ofHexColor(string)` method is provided - it accepts 6 or 8 hex digits prefixed by the `#` ```kotlin val red = ofHexColor("#FF0000") val semiTransparentRed = ofHexColor("#55FF0000") ``` > Note: when using resources colors mind that they are resolved at the moment of building theme, **NOT** at the moment of accessing. #### Drawables For the properties accepting StorytellerDrawable type. You can use `drawableRes` helper method to set a drawable resource. ```kotlin val myTheme = buildTheme { light.engagementUnits.poll.selectedAnswerBorderImage = drawableRes(R.drawable.gradient_border) } ``` #### Fonts To support multiple weights for fonts, a font family xml resource is required. The SDK will automatically select a font for the appropriate weight when needed. - Creating a font family resource `custom_font.xml` ```xml ``` - Setting the font family as custom font property `theme.font` using the helper method `fontRes` ```kotlin font = fontRes(R.font.custom_font) ``` ## Example ```kotlin val storyRowView = StoryRowView(context) storyRowView.theme = buildTheme { light { colors { primary = ofHexCode("#FF0000") success = ofHexCode("#00FF00") alert = ofHexCode("#C50511") } font = fontRes(R.font.custom_font) lists { row { tileSpacing = 8 startInset = 12 endInset = 12 } grid { tileSpacing = 8 columns = 2 } enablePlayerOpen = false // Handle tile taps manually via StorytellerDelegates } tiles { title { textSize = 11 lineHeight = 13 alignment = Gravity.START } circularTile { unreadIndicatorGradient = UiTheme.Theme.Gradient( startColor = getColor(R.color.gradient_start), endColor = getColor(R.color.gradient_end), startPosition = UiTheme.Theme.Gradient.GradientPosition.CenterLeft, endPosition = UiTheme.Theme.Gradient.GradientPosition.CenterRight, ) liveChip { unreadBackgroundGradient = UiTheme.Theme.Gradient( startColor = getColor(R.color.gradient_start), endColor = getColor(R.color.gradient_end), startPosition = UiTheme.Theme.Gradient.GradientPosition.CenterLeft, endPosition = UiTheme.Theme.Gradient.GradientPosition.CenterRight, ) readTextColor = getColor(R.color.read_live_text) unreadTextColor = getColor(R.color.unread_live_text) } } rectangularTile { padding = 8 unreadIndicator.alignment = Gravity.END unreadIndicator.textColor = ofHexCode("#000FF") unreadIndicator.textSize = 11 liveChip { readTextColor = getColor(R.color.read_live_text) unreadTextColor = getColor(R.color.unread_live_text) } } } buttons.cornerRadius = 24 buttons.textCase = TextCase.UPPER instructions { icons { forward = drawableRes(R.drawable.ic_forward_light) pause = drawableRes(R.drawable.ic_pause_light) back = drawableRes(R.drawable.ic_back_light) move = drawableRes(R.drawable.ic_move_light) } } home { title { font = fontRes(R.font.font) textCase = TextCaseTheme.LOWER textSize = 28 lineHeight = 28 textColor = getColor(R.color.rams_storyteller_primary) } } } dark from light } ``` ## AI notes / pitfalls / constraints - Global theme should be set before list views are inflated. - Per-list theme overrides the global theme. - Some list settings affect behavior (for example enablePlayerOpen). - Defaults apply when properties are not set. ## Related topics - StorytellerLists - StorytellerListViews --- # Implementing Storyteller Delegate Callbacks Source: public-docs/StorytellerDelegates.md ## Summary - StorytellerDelegate handles global analytics, ads, and in-app navigation callbacks. - onUserActivityOccurred receives analytics events and data. - getAd supplies custom ads when CMS is set to Integrating App. - StorytellerListViewDelegate handles row and grid load and tap callbacks. ## Showcase examples - [Compose — global delegate (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L32) - [Compose — list delegate example (`PageItemStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/storyteller/PageItemStorytellerDelegate.kt#L14) - [XML — global delegate (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L28) - [XML — list delegate example (`StorytellerViewDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/adapter/StorytellerViewDelegate.kt#L13) ## StorytellerDelegate To use global `StorytellerDelegate`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object. Example: ```kotlin Storyteller.storytellerDelegate = myCustomStorytellerDelegate ``` ### onUserActivityOccured The `onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData)` method is is called when an analytics event is triggered. See the dedicated Analytics page for more information on analytic events. ### getAd The `getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit)` method is called when the tenant is configured to request ads from the containing app and the SDK requires ad data from the containing app. For more information on how to supply ads to the Storyteller SDK, see the dedicated Ads page. ### userNavigatedToApp The `userNavigatedToApp(url: String)` method is called when a user taps on an action button on a Page which should direct the user to a specific place within the integrating app. More information on `In App` links Navigating to App. For more information on deep linking, see the dedicated Deep Linking page. ## Analytics The callback `onUserActivityOccurred` provides analytics events and corresponding data triggered internally by the SDK. This information can be used in your app. The following parameters are passed to the callback method: - `type` - type of event that occurred, as a `StorytellerUserActivity.EventType` enum - `data` - an object containing data about the event which occurred Example: ```kotlin ... override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) { if (type == StorytellerUserActivity.EventType.OPENED_STORY) { // Retrieve the story id value val openStoryId = data.storyId // Retrieve the story title value val openStoryTitle = data.storyTitle // Report retrieved values from your app } } ``` For a detailed discussion of all the relevant events and properties please see the dedicated Analytics page. ## Client Ads By implementing `getAd`, you can provide custom ad data for the SDK to render, this is only applicable when the ad configuration is set to `Integrating App` in the CMS. Ad data can be obtained asynchronously, and should be provided using the `onComplete` closure parameter. Example: ```kotlin ... override fun getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit) { // Action to get some ads val ad = getMyAd() // Convert the ad to a StorytellerAd val storytellerAd = convertToStorytellrAd(ad) // Provide the ad to the SDK onComplete(ad) } ``` For a detailed discussion of all the relevant considerations, please see the dedicated Ads page. ### configureWebView This method allows you to configure the WebView with custom settings or actions when the Storyteller SDK, is to display a WebView. It takes three parameters: the WebView to configure, an optional URL string, and an optional favicon Bitmap. This method is called from the `onPageStarted` method of a custom `WebViewClient` when a new page starts loading in the WebView. #### Parameters - `view: WebView` - The WebView instance to be configured. - `url: String?` - (Optional) The URL string associated with the WebView page that is being loaded. - `favicon: Bitmap?` - (Optional) The favicon Bitmap to be displayed for the WebView page. #### Example Usage To intercept and customize WebViews created by the Storyteller SDK, you can implement your own version of the `configureWebView` method. Here is an example of how to do this: ```kotlin override fun configureWebView( view: WebView, url: String?, favicon: Bitmap? ) { // Your custom configuration code goes here // The following line shows a simple alert in the WebView with the message "test webview javascript". view.evaluateJavascript("javascript: alert('test webview javascript');", null) } ``` ## Followable Categories The method `fun categoryFollowActionTaken(category: Category, isFollowing: Boolean)` is invoked when a user adds or removes a category from within SDK's UI. The callback method receives the following parameters: - `category` - An object representing the clip category - `isFollowing` - A boolean value indicating whether the user is following or unfollowing the specified category ### customScreenForCategory The `fun customScreenForCategory(): @Composable (StorytellerFollowableCategoryCustomScreen) -> Unit` method is called when a user navigates to a followable category screen. The callback receives a `StorytellerFollowableCategoryCustomScreen` object. with the following parameters: - `pendingModifier` - a `Modifier` object that is used to apply custom styling to the screen, this must be applied to the root composable of your custom screen - `category` - a `Category` object that is the category that the user is navigating to - `onBackClicked` - a `() -> Unit` a callback that should be called when the user clicks the back button from the custom screen, this is to let Storyteller know that the user has navigated back to the previous screen. If this is not called, the user will be stuck on the custom screen and unable to navigate back to the previous screen. Example: ```kotlin override fun customScreenForCategory(): @Composable() ((StorytellerFollowableCategoryCustomScreen) -> Unit) { return { (pendingModifier, category, onBackClicked) -> Scaffold( modifier = pendingModifier, topBar = { CenterAlignedTopAppBar( title = { Text("${category.displayTitle}") }, navigationIcon = { IconButton(onClick = onBackClicked) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back" ) } } ) } ) { paddingValues -> Surface( modifier = Modifier.padding(paddingValues) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center ) { Text("Custom layout for ${category.displayTitle}") } } } } } ``` Alongside the `customScreenForCategory` callback, the `Storyteller` object also has a `useCustomScreenForCategory` property that is a `(Category) -> Boolean` callback. This is to allow the developer to conditionally show the custom screen for a specific category or default to the original implementation of Followable Categories by Storyteller. The callback must return a `Boolean` value, which determines whether the custom screen should be shown for the given category. Example: ```kotlin Storyteller.useCustomScreenForCategory = { if (it.type == "custom") { // return true if you want to show the custom followable category screen true } else { // return false if you want to show the default followable category screen false } } ``` The `customScreenForCategory` will only be called if the `useCustomScreenForCategory` callback returns `true` for the given category. Otherwise, if it's not set or returns `false`, the default implementation of the Followable Categories by Storyteller will be used. ### bottomSheetScreen The `fun bottomSheetScreen(urlProvider: () -> String, onDismiss: () -> Unit): @Composable () -> Unit` method is called when the deep link includes the query parameter `shouldUseModal=true`. - `urlProvider` A callback that provides the URL to be loaded in the WebView or used for other purposes. - `onDismiss` A callback invoked when the user dismisses the bottom sheet. Example: ```kotlin override fun bottomSheetScreen( urlProvider: () -> String, onDismiss: () -> Unit ): @Composable (() -> Unit) { return { DemoBottomSheet(urlProvider, onDismiss) } } ``` ## StorytellerListViewDelegate A `StorytellerListViewDelegate` has methods for managing `StorytellerStoriesRowView` and `StorytellerStoriesGridView` events. ### How to Use To use `StorytellerListViewDelegate`, implement the `StorytellerListViewDelegate` interface and override the required methods: #### onDataLoadStarted The `onDataLoadStarted()` method is called when the network request to load data for all Stories has started. #### onDataLoadComplete The `onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int)` method is called when the data loading network request is complete. | Property | Description | | ----------- | ------------------------------------------------------- | | `success` | This confirms whether or not the request was successful | | `error` | The HTTP error if the request was not successful | | `dataCount` | The number of Stories loaded | #### onTileTapped The `onTileTapped(tileType: StorytellerTileType)` method is called when a user taps on a tile inside a row or grid. This callback is executed before the player Activity is opened. | Property | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tileType` | A sealed class that contains tile information. Can be either `StorytellerTileType.Story` (with storyId and categories) or `StorytellerTileType.Clip` (with clipId, collectionId, and categories). Categories are `List`. | Example: ```kotlin override fun onTileTapped(tileType: StorytellerTileType) { when (tileType) { is StorytellerTileType.Story -> { // Handle story tile tap val storyId = tileType.id val categories = tileType.categories } is StorytellerTileType.Clip -> { // Handle clip tile tap val clipId = tileType.id val collectionId = tileType.collectionId val categories = tileType.categories } } } ``` > Note: When `theme.lists.enablePlayerOpen` is set to `false`, this callback becomes the primary way to handle tile interactions, as the SDK will not automatically open the player. #### onPlayerDismissed The `onPlayerDismissed()` method is called when user closes Storyteller Player. Example: ```kotlin val StorytellerStoriesRowView = StorytellerStoriesRowView() StorytellerStoriesRowView.delegate = myDelegate ``` #### Error Handling By using the callback function `onDataLoadComplete` and the data it provides, you can handle the current state of the `StorytellerStoriesRowView` appropriately in your app. > Note: `dataCount` is the total number of Stories in the existing `StorytellerStoriesRowView` at any given time Example: ```kotlin ... override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (success) { // stories data has been loaded successfully // dataCount is the current total number of stories, including newly added/removed data } else if (error != null) { // an error has occurred, unwrap the error value for more information // dataCount is the total number of stories before loading began } } ``` Another example: ```kotlin ... override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (error != null && dataCount == 0) { // stories have failed to load with error and there is no data to show // you may wish to hide the `StorytellerStoriesRowView` instance here StorytellerStoriesRowView.visibility = View.GONE } } ``` ## AI notes / pitfalls / constraints - Storyteller.storytellerDelegate must be set to receive global callbacks. - customScreenForCategory must apply pendingModifier and call onBackClicked. - useCustomScreenForCategory gates when custom followable category UI is shown. - bottomSheetScreen is used when deeplink includes shouldUseModal=true. ## Related topics - Analytics - Ads - Navigating to App - Deep Linking --- # StorytellerModule Source: public-docs/StorytellerModule.md ## Summary - StorytellerModule interface handles analytics and ad callbacks for the SDK. - Implement onUserActivityOccurred, getAd, and getBottomBannerAd. - Register modules via Storyteller.modules when initializing. The StorytellerModule module is a interface you can adopt to handle the fetching of ads and recording of user activity events from Storyteller. It contains the following methods: ## Showcase examples - [Compose — initializing modules (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L61) - [XML — initializing modules (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L62) ## Methods ```kotlin abstract fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) abstract fun getAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (String) -> Unit ) fun getBottomBannerAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (String) -> Unit ) ``` ### Analytics The callback `onUserActivityOccurred` provides analytics events and corresponding data to be triggered internally by the SDK. This information can be used in your app. The following parameters are passed to the callback method: - `type` - type of event that occurred, as a `StorytellerUserActivity.EventType` enum - `data` - an object containing data about the event which occurred Example: ```kotlin override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) { when (type) { StorytellerUserActivity.EventType.AD_ACTION_BUTTON_TAPPED -> onAdAction(data) StorytellerUserActivity.EventType.OPENED_AD -> onAdStart(data) StorytellerUserActivity.EventType.FINISHED_AD -> onAdEnd(data) else -> Log.d("StorytellerModule", "Unhandled event type: $type") } } ``` See the Analytics page for more information. ### Ads By implementing `getAd` and `getBottomBannerAd`, you can provide custom ad data for the SDK to render. This is only applicable when the ad configuration is set to `Integrating App` in the CMS. Ad data can be obtained asynchronously, and should be returned via the appropriate callback. #### getAd The `getAd` method is called when the SDK needs a fullscreen ad. The `onComplete` callback should be called when the ad data is ready, and the `onError` callback should be called if an error occurs. ```kotlin override fun getAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (String) -> Unit ) { val ad = fetchAd(adContext, adRequestInfo) if (ad != null) { onComplete(ad) } else { onError("Failed to fetch ad") } } ``` #### getBottomBannerAd The `getBottomBannerAd` method is called when the SDK needs a bottom banner ad (displayed at the bottom of clips). The `adContext.isBottomBanner` property will be `true` for these requests. Only 300x50 and 320x50 ad sizes are supported. ```kotlin override fun getBottomBannerAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (String) -> Unit ) { val bannerAd = fetchBottomBannerAd(adContext, adRequestInfo) if (bannerAd != null) { onComplete(bannerAd) } else { onError("Failed to fetch bottom banner ad") } } ``` For a detailed discussion of all the relevant considerations, please see the dedicated Ads page. ## Technical Consideration Use of a custom StorytellerModule is optional. If you do not implement this interface, the SDK will use the default implementation see Ads. It must be provided to the SDK when initializing the Storyteller SDK using the `Storyteller.modules` method to see Gam Ads. ## AI notes / pitfalls / constraints - getBottomBannerAd is used for clips bottom banner placement. - Only 300x50 and 320x50 banner sizes are supported. - Module is optional; SDK uses the default implementation if not provided. ## Related topics - Analytics - Ads --- # Navigating to App Source: public-docs/NavigatingToApp.md ## Summary - Implement StorytellerDelegate.userNavigatedToApp to handle deeplink actions. - The callback passes the URL configured in CMS for a page action button. - Your app parses the URL and navigates to the correct location. A `StorytellerDelegate` has method for managing in app navigation. It can be used for custom handling of `deeplink` URLs which can configured per page in CMS. This method will be called when user presses on the action button with the `deeplink` navigation type. ## Showcase examples - [Compose — `userNavigatedToApp` (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L117) - [XML — `userNavigatedToApp` (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L82) ## How to Use To use global `userNavigatedToApp`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object. `userNavigatedToApp(url: String)` is the method responsible for handling these `deeplink` URLs. ## Navigation to the Integrating App The callback `userNavigatedToApp` is called when a user taps on an action button on a page which has its link type set to `deeplink`. In this case, your app will be passed a URL which has been entered in the Storyteller CMS and your app is then responsible for parsing this URL and following it to the correct location within your app. Example: ```kotlin override fun userNavigatedToApp(url: String) { // parse the url and navigate to the destination } ``` ## AI notes / pitfalls / constraints - userNavigatedToApp fires only for action links with deeplink type. - Storyteller.storytellerDelegate must be set to receive the callback. --- # Analytics Source: public-docs/Analytics.md ## Summary - Analytics events are emitted as StorytellerUserActivity.EventType values with event data. - Events cover Stories, Clips, Sheets, Cards, Ads, Playback, and Search. - Event data includes IDs, timing, actions, and context metadata. ## Showcase examples - [Compose — map `onUserActivityOccurred` to Amplitude](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/amplitude/AmplitudeAnalyticsManager.kt#L19) - [XML — map `onUserActivityOccurred` to Amplitude](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/amplitude/AmplitudeAnalyticsManager.kt#L18) ## Event Types These are the various events which are triggered from within the SDK. Each event is a member of the `StorytellerUserActivity.EventType` enumeration. Please see the StorytellerDelegates page for details on how to bind to these events. In the below discussion, "completing" a Page refers to allowing the timer to expire - so this would correspond to watching all of an image or a Poll Page for the duration set for it (default 15s) or watching all of a video. ## Story Events ### Opened Story This event is recorded in the following scenarios: - When a user taps on a list item to open a Story - When a Story is loaded because the previous Story finished - When a Story is loaded because the user tapped to skip the last Page of the previous Story - When a user swipes left on a Story to go to the next Story - When a user swipes right on a Story to go to the previous Story - When a user is sent directly to a Story via a deep link Whenever an Opened Story event occurs, event data for `storyID`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyReadStatus`, `storyPlaybackMode`, `openedReason`, `currentCategory`, `collection`, `categories`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Opened Page This event is recorded as soon as a user sees content for a Page. Opened Page is one of the most important events to track as it is equal to a video start, one of the most important measures of engagement. By tracking this event, you can monitor valuable information about user engagement with your app. Whenever an Opened Page event occurs, event data for `pageID`, `pageIndex`, `pageType`, `storyPlaybackMode`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `contentLength`, `storyID`, `storyTitle`, `storyIndex`, `storyPageCount`, `currentCategory`, `collection`, `openedReason`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Story This event is recorded in the following scenarios: - When a user taps the close button to dismiss the Story - When a user swipes down to dismiss the Story - When a user taps back on their device UI to dismiss the Story (Android only) - When a user taps to skip the last Page of the final Story - this dismisses the Story and exits the Story View - When a user swipes left on the final Story to dismiss the Story - When a user swipes right on the first Story to dismiss the Story - When a user completes the final Page of the final Story and the Story view is dismissed Whenever a Dismissed Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType` `dismissedReason`, `durationViewed`, `currentCategory`, `collection`, `pagesViewedCount`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Skipped Story This event is recorded when: - a user swipes left to go to the next Story - a user skips the last Page of a Story to go to the next Story Whenever a Skipped Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Skipped Page This event is recorded when a user taps to go to the next Page before completing the current Page. Whenever a Skipped Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Story This event is recorded at the same time as `OpenedPage` for the final Page in a Story. Whenever a Completed Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Page This event is recorded when a user watches a Page to completion (i.e. the timer for that Page finishes). Whenever a Completed Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `contentLength`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Action Button Tapped This event is recorded when a user taps on an action button on a Page to open a link. Whenever a Action Button Tapped event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `collection`, `actionText`, `pageActionUrl`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Button Tapped This event is recorded when a user taps the share button on a Page. Whenever a Share Button Tapped event occurs, event data for `shareMethod`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Story This event is recorded when: - a user swipes right to go to the previous Story (unless this is the first Story - in which case DismissedStory is fired instead) - a user taps back on the first Page in a Story (and this is not the first Page in the first Story) Whenever a Previous Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Page This event is recorded when a user taps back to go to a previous Page in the Story. Whenever a Previous Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Success This event is recorded when a user selects a sharing method from the system dialog. Note: this event is only available for Android API level 22 and above. Whenever a Share Success event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `shareMethod`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `isMuted` and `context` is returned. > Note 1: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. > Note 2: sharing to clipboard does not trigger Share Success event, only when an app is selected for sharing the event will be fired. ### Voted Poll This event is recorded when a user votes in a poll. Whenever a Voted Poll event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageId`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `pollAnswerId`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Trivia Quiz Question Answered This event is recorded when a user submits a Quiz answer. Whenever a Trivia Quiz Question Answered event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `triviaQuizId`, `triviaQuizTitle`, `triviaQuizQuestionId`, `currentCategory`, `collection`, `triviaQuizAnswerId`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Trivia Quiz Completed This event is recorded when a user completes a Quiz. Whenever a Trivia Quiz Completed event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `triviaQuizId`, `triviaQuizTitle`, `currentCategory`, `collection`, `triviaQuizScore`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Muted Story This event is recorded when a user taps the mute button to mute audio while viewing a Story. Whenever a Muted Story event occurs, event data for `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyId`, `storyIndex`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `pageHasAction`, `pageActionType`, `pageId`, `pageIndex`, `pageTitle`, `pageType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyPlaybackMode`, `storyTitle`, `isMuted` and `context` is returned. ### Unmuted Story This event is recorded when a user taps the mute button to unmute audio while viewing a Story. Whenever an Unmuted Story event occurs, event data for `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyId`, `storyIndex`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `pageHasAction`, `pageActionType`, `pageId`, `pageIndex`, `pageTitle`, `pageType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyPlaybackMode`, `storyTitle`, `isMuted` and `context` is returned. ## Clip Events ### Opened Clip This event is recorded when: - a user taps on a row or grid item to open a Clip - a user swipes up to the next Clip - a user swipes down to the previous Clip - a user is sent directly to a Clip via a call to `openCollection` - a user is sent directly to a Clip via a deep link - a user dismisses the last category (by pressing back) and returns to the top level collection Whenever a Opened Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `clipHasAction`, `clipActionType`, `openedReason`, `collection`, `categories`, `contentLength`, `clipFeedType`, `categoryDetails`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Clip This event is recorded when: - a user taps on the back button in the top-left to exit the Clips player (and is at the top of the stack of Clip Categories) - a user taps back on their device UI to navigate back (Android only) This event does not fire when the user is not at the top of the Clip Category stack. Whenever a Dismissed Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `dismissedReason`, `durationViewed`, `clipsViewed`, `collection`, `categories`, `clipFeedType`, `loopsViewed`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Next Clip This event is recorded when a user swipes up to go to the next Clip. Whenever a Next Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Clip This event is recorded when a user swipes down to go to the previous Clip. Whenever a Previous Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Loop This event is recorded when a user completes a loop of a Clip. Whenever a Completed Loop event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `completionType`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired. ### Action Button Tapped This event is recorded when: - a user taps the primary action button at the bottom of a Clip - a user taps any secondary action buttons above the clip title - a user swipes left on a Clip to open the relevant action Whenever an Action Button Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `clipHasAction`, `clipActionType`, `clipActionText`, `collection`, `categories`, `clipFeedType`, `actionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `actionClass`, `actionIndex`, `tappedClipActionText`, `tappedClipActionUrl`, `tappedClipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Button Tapped This event is recorded when a user taps the share button on a Clip. Whenever a Share Button Tapped event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Success This event is recorded when a user selects a sharing method from the system dialog. Whenever a Share Success event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `shareMethod`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. ### Paused Clip This event is recorded when a user taps on the screen whilst a Clip is playing to pause the Clip. Tt does not fire when a Clip is paused automatically by sharing or following an action. Whenever a Paused Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired, as pausing/resuming live clips is not supported. ### Resumed Clip This event is recorded when a user taps a Clip that is paused to resume playback. It does not fire when a Clip is resumed automatically. Whenever a Resumed Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired, as pausing/resuming live clips is not supported. ### Liked Clip This event is recorded when a user likes a Clip by tapping the like button when they do not currently like the Clip. Whenever a Liked Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Unliked Clip This event is recorded when a user unlikes a Clip by tapping the like button when they currently like the Clip. Whenever a Unliked Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Finished Clip This event is recorded when: - Dismissed Clip is fired - Next Clip is fired - Previous Clip is fired - At the same time as OpenedCategory - (iOS specific triggers exist for loop completion, For You/Following switching) Whenever a Finished Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `loopsViewed`, `durationViewed`, `collection`, `categories`, `clipFeedType`, `contentLength`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `completionType`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Opened Category This event is recorded when: - a user taps a category at the bottom of the Clips Player to open a new Category - a user opens a category by navigating back from another category This event is not fired when opening the top level of a collection - only when navigating to a specific category. Whenever a Opened Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Category This event is recorded when: - a user navigates back using the back button (or system back on Android) to the previous category. This event does not fire when dismissing the top level collection Whenever a Dismissed Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Follow Category This event is recorded when: - a user taps the plus button under the follow category button of the Clips Player - a user taps the follow button at the top right of the Followable Category screen and the category was not followed Whenever a Follow Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Unfollow Category This event is recorded when: - a user taps the checkmark button under the follow category button of the Clips Player - a user taps the follow button at the top right of the Followable Category screen and the category was followed Whenever a Unfollow Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Tapped This event is recorded when: - a user taps the follow category button of the Clips Player to open the Followable Category screen - a user swipes left from the right edge of the screen on a Clip with a Followable Category and opens the Followable Category screen Whenever a Followable Category Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Enable Captions This event is recorded when: - a user taps the button to enable Closed Captions in the Clips Player Whenever an Enable Captions event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Disable Captions This event is recorded when: - a user taps the button to disable Closed Captions in the Clips Player Whenever a Disable Captions event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Shown This event is recorded when: - a user attempts to follow a category but has reached their following limit and the limit dialog is displayed Whenever a Followable Category Limit Shown event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Action Button Tapped This event is recorded when: - a user taps the action button in the followable category limit dialog Whenever a Followable Category Limit Action Button Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Dismissed This event is recorded when: - a user dismisses or closes the followable category limit dialog without taking action Whenever a Followable Category Limit Dismissed event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Muted Clip This event is recorded when a user taps the mute button to mute audio while viewing a Clip. Whenever a Muted Clip event occurs, event data for `captionsEnabled`, `actionLinkId`, `categories`, `categoryDetails`, `clipHasAction`, `clipActionText`, `clipActionUrl`, `clipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `clipId`, `clipIndex`, `clipTitle`, `collection`, `contentLength`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isLive`, `isMuted` and `context` is returned. ### Unmuted Clip This event is recorded when a user taps the mute button to unmute audio while viewing a Clip. Whenever an Unmuted Clip event occurs, event data for `captionsEnabled`, `actionLinkId`, `categories`, `categoryDetails`, `clipHasAction`, `clipActionText`, `clipActionUrl`, `clipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `clipId`, `clipIndex`, `clipTitle`, `collection`, `contentLength`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isLive`, `isMuted` and `context` is returned. ## Sheet Events ### Opened Sheet This event is recorded in the following scenarios: - When a user taps on an action button on a clip or story to open a Sheet - When a Sheet is opened via a deep link - When a Sheet is opened via the `openSheet` method Whenever a user taps on an action button on a clip or story to open a Sheet the Opened Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyPlaybackMode`, `currentCategory`, `collection`, `categories`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `clipId`, `clipTitle`, `clipIndex`, `clipHasAction`, `clipActionType`, `clipActionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `metadata`, `isMuted` and `context` is returned. Whenever a Sheet is opened via a deep link or via the `openSheet` method the Opened Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Sheet This event is recorded when a user closes a Sheet by: - Tapping outside of the Sheet (for 50% height setting Sheets) - Dragging the sheet down beyond a threshold - Tapping the back button - Tapping the system back button Whenever a user dismisses the Sheet on a clip or story the Dismissed Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyPlaybackMode`, `currentCategory`, `collection`, `categories`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `clipId`, `clipTitle`, `clipIndex`, `clipHasAction`, `clipActionType`, `clipActionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `metadata`, `isMuted` and `context` is returned. Whenever a user dismisses the Sheet that is opened via a deep link or via the `openSheet` method the Dismissed Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `metadata`, `isMuted` and `context` is returned. ## Card Events ### Card Tapped This event is recorded when: - a user taps on the Storyteller Card view Whenever a Card Tapped event occurs, event data for `cardId`, `cardActionType`, `cardActionUrl`, `cardAspectRatio`, `cardBackgroundType`, `cardCollectionId`, `cardIndex`, `cardSubtitle`, `cardTitle`, `categories`, `categoryDetails`, `hasButton`, and `context` is returned. ### Card Viewed This event is recorded when: - the background image of the card closest to the center of the screen loads. For video cards, this event is triggered when the video starts playing. Whenever a Card Viewed event occurs, event data for `cardId`, `cardActionType`, `cardActionUrl`, `cardAspectRatio`, `cardBackgroundType`, `cardCollectionId`, `cardIndex`, `cardSubtitle`, `cardTitle`, `categories`, `categoryDetails`, `hasButton`, and `context` is returned. ## Ad Events ### Opened Ad #### Stories This event is recorded when: - an ad is loaded because the previous Story finished - a user swipes left on a Story to go to the next Story and an ad appears - a user swipes right on a Story to go to the previous Story and an ad appears Whenever a Opened Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `openedReason`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when: - a user swipes up on a clip to go to the next clip and an ad should appear next - a user swipes down on a clip to go to the previous clip and an ad should appear next - an ad completes a loop and begins playing again from the start Information about Ad View is only sent to client delegates on an OpenedAd event. Whenever a Opened Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `openedReason`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Dismissed Ad #### Stories This event is recorded when a user: - taps close to dismiss the ad - swipes down to dismiss the ad - taps back on their device UI to dismiss the ad (Android only) - taps to skip the ad if the ad is the last Page in the current set of Stories - swipes left on the ad to skip it if the ad is the last Page in the current set of Stories - completes the ad if the ad is the last Page in the current set of Stories Whenever a Dismissed Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `dismissedReason`, `durationViewed`, `pagesViewed`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user: - taps the back button to exit the clips player when an ad is being shown - (Android only) presses the system back button to exit the clips player when an ad is being shown Whenever a Dismissed Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `dismissedReason`, `durationViewed`, `pagesViewed`, `clipsViewed`, `loopsViewed`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Skipped Ad #### Stories This event is recorded when a user: - swipes left to go to the next Story before completing the current ad - taps to go to the next Page on an ad before completing the current ad Whenever a Skipped Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when: - a user swipes up to go to the next clip - a user swipes down to go the previous clip Whenever a Skipped Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Ad Action Button Tapped #### Stories This event is recorded when a user taps on an action button on an ad to open a link. Whenever a Ad Action Button Tapped event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `actionText`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user: - taps the action button at the bottom of an ad displayed in clips - swipes left on a clip to open the relevant action Whenever a Ad Action Button Tapped event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Previous Ad This event is recorded when a user: - swipes right to go to the previous Story when viewing an ad - taps back to go to the previous Page when viewing an ad Whenever a Previous Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. ### Finished Ad #### Stories This event is recorded at the same time as Dismissed Ad, Skipped Ad, Previous Ad and Viewed Ad Page Complete and gives an easier way to determine when an ad finishes for any reason. Whenever a Finished Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded at the same time as DismissedAd, or SkippedAd. Whenever a Finished Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Paused Ad #### Stories This event is recorded when a user pauses a Page within an ad by pressing and holding on the Page. Whenever a Paused Ad Page event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user pauses a clip which is an ad by tapping the screen. It does not fire when a clip is paused automatically for sharing or following an action Whenever a Paused Ad Clip event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Resumed Ad #### Stories This event is recorded when a user resumes playing a Page within an ad by releasing their long press which paused the ad. Whenever a Resumed Ad Page event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is fired when a clip which is an ad is paused and a user taps the screen to resume playback. It does not fire when a clip is resumed automatically Whenever a Resumed Ad Clip event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page First Quartile This event is recorded when a user reaches 1/4 of the way through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page First Quartile event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Midpoint This event is recorded when a user reaches halfway through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Midpoint event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Third Quartile This event is recorded when a user reaches 3/4 of the way through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Third Quartile event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Complete This event is recorded when a user reaches the end of the duration for a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Complete event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Muted Ad This event is recorded when a user taps the mute button to mute audio while viewing an Ad. #### Stories Whenever a Muted Ad event occurs in Stories, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `adView`, `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyIndex`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyTitle`, `isMuted` and `context` is returned. #### Clips Whenever a Muted Ad event occurs in Clips, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `categories`, `contentLength`, `openedReason`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isMuted` and `context` is returned. ### Unmuted Ad This event is recorded when a user taps the mute button to unmute audio while viewing an Ad. #### Stories Whenever an Unmuted Ad event occurs in Stories, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `adView`, `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyIndex`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyTitle`, `isMuted` and `context` is returned. #### Clips Whenever an Unmuted Ad event occurs in Clips, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `categories`, `contentLength`, `openedReason`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isMuted` and `context` is returned. ## Playback Events ### Ready to Play This event is called once per video Page at the point when the video player has been loaded. Whenever a Ready to Play event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isLive`, `metadata` and `context` is returned. ### Media Started This event is called once per video Page at the point when the video starts to play for the first time. Whenever a Media Started event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isLive`, `metadata` and `context` is returned. ### Buffering Started This event is called on video Pages whenever the video starts to buffer. Whenever a Buffering Started event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isInitialBuffering`, `isLive`, `metadata` and `context` is returned. ### Buffering Ended This event is called on video Pages whenever the video finishes buffering > Note: There should be at most one Ready to Play event and one Media Started event for every Page. There may be multiple Buffering Started/Buffering Ended pairs of events for an individual Page. There may not always be a Buffering Ended event for every Buffering Started event as the user may choose to exit the Page during buffering. Whenever a Buffering Ended event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isInitialBuffering`, `timeSinceBufferingBegan`, `isLive`, `metadata` and `context` is returned. ## Search Events ### Opened Search The openedSearch event is recorded when: - A user taps on Search from a Story within the Story player - A user taps on Search from a Clip within the Clip player ### Dismissed Search The dismissedSearch event is recorded when a user taps the 'X' button to exit the Search interface. ### Performed Search The performedSearch event is recorded when: - A user taps the 'Search' icon after entering a term in the Search bar (whether manually or by tapping the 'arrow' icon beside a Search suggestion to populate the search bar) - A user taps the 'Search' icon beside a Search suggestion. The Search is then performed with the suggestion as the term. - A user taps 'Apply filters' from the filters interface. ### Opened Filters The openedFilters event is recorded when, after a user has performed as Search, they press the 'Filter' icon to bring up the filter interface. > Note that filters can only be applied after the initial search has been performed. ### Dismissed Filters This event is recorded when a user, after calling up the 'Filters' interface, swipes down to exit the interface without applying any. ### Used Suggestion The usedSuggestion event is recorded when: - A user taps the 'arrow' icon beside a Search suggestion to populate the Search bar. This does not trigger any other event, and the user may amend the Search bar input before performing a Search. - A user taps the 'Search' icon beside a Search suggestion. This simultaneously triggers a PerformedSearch event, above. ## Event Data For each event, data is returned with details about the story and Page involved as well as some extra properties with more information about what the user has done. The data is returned as a `UserActivityData` class with the following properties: ### Story ID The `storyId: String?` is the ID of the Story for which the event occurred. ### Story Title The `storyTitle: String?` is the title of the Story for which the event occurred. ### Story Display Title The `storyDisplayTitle: String?` is the display title of the Story for which the event occurred. ### Story Index The `storyIndex: Int?` is the index of the Story for which the event occurred in the row from which it was opened at the point it was opened - this is only included on `OpenedStory` events. Note: this value is 1-based. ### Story Page Count The `storyPageCount: Int?` is the number of Pages in the Story. ### Story Read Status The `storyReadStatus: String?` is whether the Story was read or unread at the point the Story was opened - this is only included on `OpenedStory` events. Note this will either be `read` or `unread`. ### Page ID The `pageId: String?` is the ID of the Page for which the event occurred. ### Page Index The `pageIndex: Int?` is the index of the Page in the Story for which the event occurred. Note: this value is 1-based. ### Page Type The `pageType: String?` is the type of the Page associated with the event. This can have the value `image`, `video` or `poll`. ### Story Playback Mode The `storyPlaybackMode: String?` value states if the Story was opened during the list or in the single Story mode (Storyteller static method.) This is included for all events. The values for this are either `list` or `singleStory`. ### Page Has Action The `pageHasAction: Bool?` value states whether the Page associated with the event contains an action. ### Page Action Type The `pageActionType: String?` is the type of the action on the page. ### Page Action Text The `pageActionText: String?` is the text call to action if the Page has an Action. ### Page Action URL The `pageActionUrl: String?` is the URL for the link if the Page has an Action. ### Opened Reason The `openedReason: String?` value states how the user opened a Story or Clip. The possible values for this are `storyRowTap` (the user tapped the Story in the Story row); `deepLink` (openStory or openPage was called to open the Story); `swipe` (the user swiped left or right to change the current Story); `automaticPlayback` (the user completed the previous Page); `tap` (the user tapped on the previous Page to navigate to this Page); `clipListTap` (the user tapped the Clip in a Clip list) and `loop` (a Clip has completed a full playback cycle and started playing again). `openedReason` is only included on `OpenedStory` and `OpenedClip` events. ### Dismissed Reason The `dismissedReason: String?` value states the way the user dismissed a Story or Clip. The possible values for this are `closeButtonTapped` (the user tapped close to dismiss the Story); `swipedDown` (the user swiped down to dismiss the Story); `swipedFirstStory` (the user swiped the first Story to dismiss it); `swipedFinalStory` (the user swiped the final Story to dismiss it); `skippedFinalPage` (the user tapped to skip the final Page of the final Story); `backTapped` (the user tapped their device back button to dismiss the Story view); `completedFinalPage` (the user completed the final Page of the final Story) and `backButtonTapped` (the user tapped the back button to dismiss the Clip). `dismissedReason` is only included on `DismissedStory` and `DismissedClip` events. ### Duration Viewed The `durationViewed: Float?` is the duration the user viewed the Story or Clip for in milliseconds. This is measured from the most recent `OpenedStory` or `OpenedClip` event with an Opened Reason of `storyRowTap`, `deepLink` or `clipsListTap`. This timer is reset after any `DismissedStory` or `DismissedClip` events. For `FinishedClip`, Duration Viewed is the the duration the user viewed the Clips player for in milliseconds. This is measured from the most recent `OpenedClip` event with an Opened Reason of `swipe`. ### Pages Viewed Count The `pagesViewedCount: Int?` is the total number of Pages a user has viewed since the most recent `OpenedStory` event with an Opened Reason of `storyRowTap` or `deepLink`. This count is reset after any `DismissedStory` events. ### Content Length The `contentLength: Long?` is the total duration of the Page content in seconds. ### Ad ID The `adId: String?` is the ad ID if an event is associated with an ad. ### Share Method The `shareMethod: String?` is the component name of the app which the user has selected for sharing. > Note: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. ### Ad View The `adView: View?` is the view the ad is rendered in. This is only included for `OpenedAd` and `FinishedAd` events. ### Advertisers Name The `advertiserName: String?` is the name of the advertiser for a particular ad. This is only included for ad events. ### Ad Type The `adType: String?` tells the source component of the ad. It can be either `stories` or `clips`. This is only included for ad events. ### Ad Placement The `adPlacement: String?` tells the placement of the ad. It can be either `Between Stories`, `Between Pages` or `Between Clips`. This is only included for ad events. ### Ad Format The `adFormat: String?` represents the format of the Ad that was displayed. Possible values can be `customNative`, `native`, `banner`. This is only included for ad events. ### Ad Response Identifier The `adResponseIdentifier: String?` represents the response identifier attached to the ad that was received from the ad provider. Used for debugging ad targeting. This is only included for ad events. ### Is Initial Buffering The `isInitialBuffering: Boolean?` value is returned if the buffering happens at the start of playback for that Page. This is only included for `BufferingStarted` and `BufferingEnded` events. ### Time Since Buffering Began The `timeSinceBufferingBegan: Long?` value is the duration the current buffering lasted for in milliseconds. This is only included for `BufferingEnded` events. ### Categories The `categories: [String]?` value is the list of categories assigned to the Story for which the event occurred. This is only included on `OpenedStory` events. ### Poll Answer The `pollAnswerId: String?` is the ID of the answer the user selected when voting. This is only included on `VotedPoll` events. ### Trivia Quiz Answer ID The `triviaQuizAnswerId: String?` is the ID of the selected trivia quiz answer. This is only included on `TriviaQuizQuestionAnswered` events. ### Trivia Quiz ID The `triviaQuizId: String?` is the ID of the Trivia Quiz that was completed or answered. This is only included on `TriviaQuizQuestionAnswered` and `TriviaQuizCompleted` events. ### Trivia Quiz Question ID The `triviaQuizQuestionId: String?` is the ID of the Trivia Quiz question which was answered. This is only included on `TriviaQuizQuestionAnswered` events. ### Trivia Quiz Title The `triviaQuizTitle: String?` is the title of the Trivia Quiz that was completed or answered. This is only included on `TriviaQuizQuestionAnswered` and `TriviaQuizCompleted` events. ### Trivia Quiz Score The`triviaQuizScore: Int?` value is the score of the trivia quiz that was completed. This is only included on `TriviaQuizCompleted` events. ### Clip ID The `clipId: String?` is the ID of the Clip for which the event occurred. ### Clip Title The `clipTitle: String?`is the title of the Clip for which the event occurred. ### Clip Index The `clipIndex: Int?` is the index of the Clip in the row or grid at the point it was selected or the index of the Clip in the feed at the point it was viewed. ### Clip Collection Count The `clipCollectionCount: Int?` amount of Clips in the collection. ### Clips Viewed The `clipsViewed: Int?` value is the total number of Clips a user has viewed since the most recent OpenedClip event with an Opened Reason of `clipListTap` or `deepLink`. This count should be reset after any DismissedClip events. ### Loops Viewed The `loopsViewed: Int?` is for DismissedClip, the total number of loops (plays of an individual Clip) a user has viewed since the most recent OpenedClip event with an Opened Reason of `clipListTap` or `deepLink`. This count should be reset after any DismissedClip events. For FinishedClip, the total number of loops. ### Clip Has Action The `clipHasAction: Bool?` value is whether the Clip associated with the event contains a primary action. ### Clip Action Type The `clipActionType String?` is the type of the action on the clip. ### Clip Action Text The `clipActionText String?` is the text call to action if the Clip associated with the event has a primary Action link. ### Clip Action URL The `clipActionUrl: String?` is the URL linked to from the action if a Clip associated with the event has a primary Action. ### Action Class The `actionClass: String?` identifies whether the action button that was tapped is a primary or secondary action. Possible values are `primary` and `secondary`. This is only included for `ClipActionButtonTapped` events. ### Action Index The `actionIndex: Int?` is the 1-based index of the secondary action that was tapped. This is only included for `ClipActionButtonTapped` events when a secondary action is tapped. ### Tapped Clip Action Text The `tappedClipActionText: String?` is the text of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Tapped Clip Action URL The `tappedClipActionUrl: String?` is the URL of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Tapped Clip Action Type The `tappedClipActionType: String?` is the type of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Has Secondary Actions The `clipHasSecondaryActions: Boolean?` indicates whether the clip has any secondary actions. This is included in all Clip Analytics Events. ### Secondary Actions Text The `clipSecondaryActionsText: [String]?` is an array of all the text CTAs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Secondary Action URLs The `clipSecondaryActionUrls: [String]?` is an array of all the URLs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Secondary Action Types The `clipSecondaryActionTypes: [String]?` is an array of all the types of secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Is Live The `isLive: Boolean?` set to `true` for Live Clips. ### Action Text The `actionText` property has exactly the same value as "Page Action Text" or "Clip Action Text" and is only reported for Action Button Tapped ### Action Link ID The `actionLinkId: String?` is a unique identifier for the action link associated with pages or clips. This property is included in analytics events where actions are present and allows tracking specific action links across the platform. The action link ID can be used to identify which specific CTA (call-to-action) was interacted with or displayed, enabling more granular analytics and attribution for content performance. ### Collection The `collection` is the ID of the Collection if a Story or Clip is being played from a Collection. ### Category Details The `categoryDetails` is a list of Category Detail objects. This includes the name, ID, type and placement of the Category. ### Category Name The `categoryName` of the category being navigated to or dismissed ### Category ID The `categoryId` of the category being navigated to or dismissed. ### Current Category The `currentCategory` is the Category for the row that is currently being interacted with. The information provided from this is the Category title, ID and placement. This is only included on Story and Ad events. ### Search From The `searchFrom` indicates whether the Search for which the event is recorded was opened from Clips or Stories. ### Is Suggestion The `isSuggestion` indicates whether the Search for which the event is recorded used a suggested Search. > Note that when filters are opened (event), the Is Suggestion value should reflect what was used for the initial search performed before opening filters. ### Initial Input The `initialInput` this property tracks the input at the moment the suggestion was used. ### Search Filter The `searchFilter` property includes the content type and date posted used by search filter. ### Search Sort The `searchSort` field indicates method by which the relevant Story / Clip's search results were sorted. ### Search Term The `searchTerm` by which the Clips / Stories were searched, either entered in the search bar by the user or selected / filled from search suggestions. ### Captions Enabled The `captionsEnabled: Boolean?` value is set to `true` if captions are enabled. ### Is Muted The `isMuted: Boolean?` value indicates the current mute state of the audio. This is set to `true` when audio is muted and `false` when audio is unmuted. This property is included in mute/unmute events for Stories, Clips, and Ads. ### Sheet ID The `sheetId` is the ID of the Sheet for which the event occurred. ### Sheet Title The `sheetTitle` is the title of the Sheet for which the event occurred. ### Sheet Size The `sheetSize` is the height setting of the Sheet. This can have the value `50`, `75` or `100` (representing % of screen height). ### Card ID The `cardId: String?` is the ID of the Card for which the event occurred. This is included for `CardTapped` event. ### Card Action Type The `cardActionType: String?` is the type of the action on the Card. This is included for `CardTapped` event. ### Card Action URL The `cardActionUrl: String?` is the URL for the link if the Card has an action. This is included for `CardTapped` event. ### Card Aspect Ratio The `cardAspectRatio: String?` is the aspect ratio of the Card. This is included for `CardTapped` event. ### Card Background Type The `cardBackgroundType: String?` is the type of background on the Card. This can have the value `image` or `video`. This is included for `CardTapped` event. ### Card Collection ID The `cardCollectionId: String?` is the ID of the collection the Card belongs to. This is included for `CardTapped` event. ### Card Index The `cardIndex: Int?` is the index of the Card in the collection. Note: this value is 1-based. This is included for `CardTapped` event. ### Card Subtitle The `cardSubtitle: String?` is the subtitle text displayed on the Card. This is included for `CardTapped` event. ### Card Title The `cardTitle: String?` is the title text displayed on the Card. This is included for `CardTapped` event. ### Metadata The `metadata: Map?` is a map containing custom metadata key-value pairs associated with the content. This is included for Story, Clip, Playback, and Sheet events. The metadata comes from the Story or Clip associated with the event and can contain custom fields provided via the Storyteller API. ### Followable Category Limit Action Text The `followableCategoryLimitActionText: String?` is the text displayed on the action button in the followable category limit dialog. This is only included for followable category limit events. ### Followable Category Limit Action URL The `followableCategoryLimitActionUrl: String?` is the URL that the action button in the followable category limit dialog will navigate to when tapped. This is only included for followable category limit events. ### Followable Category Limit Dialogue The `followableCategoryLimitDialogue: String?` is the main text content displayed in the followable category limit dialog explaining why the user cannot follow more categories. This is only included for followable category limit events. ### Location The `location: String?` indicates where the followable category limit dialog was displayed. For clip-related limit events, this value is `"On Clip"`. This is only included for followable category limit events. ### Eyebrow The `eyebrow: String?` is the subtitle text displayed above the title for Stories and Clips tiles. It is also present in Stories and Clips Player assuming it is set in CMS. ### Completion Type The `completionType: String?` indicates how the user finished watching a Clip or completed a loop. This property is included in `finishedClip` and `completedLoop` events. Possible values include: - `natural` - The clip or loop completed naturally by playing to the end - `scrubbed` - The user used the progress bar to scrub/seek to the end This helps track whether users are actively engaging with the full content or skipping through it using the progress bar. ### Context The `context: StorytellerAnalyticsContext?` contains custom context data that was provided when configuring the SDK row/grid/card component. This allows integrators to track the context of where the analytics event occurred. This property is included in all Story, Clip, Ad, Media Playback, Sheet, and Card analytics events. ## AI notes / pitfalls / constraints - Completing a page means timer expiry (default 15s for images or polls) or video completion. - Share Success is only available on Android API level 22+; on API 31+ shareMethod is null. - Live clips do not fire Completed Loop, Paused Clip, or Resumed Clip events. - Some fields are only present for specific events; see the Event Data section. ## Related topics - StorytellerDelegates --- # Ads Source: public-docs/Ads.md ## Summary - Ads can be Storyteller-managed (first party) or provided via Google Ad Manager (GAM) module. - Install the Storyteller GAM module and register it via Storyteller.modules. - Provide adUnit lambda (static or dynamic) and optional template IDs or key-value pairs. - Bottom banner ads are supported for clips via bottomBannerAdUnit. ## Introduction The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager via an SDK extension developed by Storyteller and Ads from other sources via custom implementation provided by the integrator. Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team. ### Storyteller First Party Ads If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK. ### Storyteller GAM SDK To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller. You will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager. To use this extension, first install it using Gradle. Ensure you have the following Maven repository added to your `settings.gradle`: ```groovy maven { url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk") } ``` Then add the following reference to your version catalog file: ```toml storyteller-ads = { module = "Storyteller:ads", version.ref = "storyteller" } ``` And finally reference this in your `build.gradle`: ```groovy implementation(libs.storyteller.ads) ``` #### Basic Setup Now initialize the extension as follows: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.CustomNativeTemplateIds val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/33813572/storyteller" }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` You will need to supply the following parameters: | Parameter Name | Description | |--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `{ storytellerAdRequestInfo -> adUnitId` } | The lambda which returns desired `adUnitId`. This can be used for dynamic `adUnitId` changes depending on `storytellerAdRequestInfo` content. If you do not need dynamic adUnit changes simply put the static ad unit as a return value. | | `bottomBannerAdUnit` | Optional lambda that returns the Ad Unit ID used specifically for the Clips bottom banner placement. Leave this `null` if you don't plan to serve Clips bottom banner ads. | Then pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance: ```kotlin Storyteller.modules = listOf(storytellerGamModule) ``` ### Showcase examples - [Compose — GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L64) - [Compose — ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L12) - [XML — GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L64) - [XML — ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L12) #### Setup with the dynamic Ad Unit changes Example for dynamic Ad Unit changes when we want to use different adUnit for Stories and Clips: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.CustomNativeTemplateIds val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> when (storytellerAdRequestInfo) { is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories" else -> "/33813572/storyteller/clips" } }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` #### Setup with the additional parameters You can also supply optional parameters `templateIds` and `keyValuePairs` if needed: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> when (storytellerAdRequestInfo) { is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories" else -> "/33813572/storyteller/clips" } }, templateIds = StorytellerCustomNativeTemplateIds("12102683", "12269089"), keyValuePairs = { emptyMap() }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` | Parameter Name | Description | |--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `templateIds` | If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct. | | `keyValuePairs` | A function that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. **By default, Storytelles always includes the following KVPs:** `stApiKey`(current API key), `stCollection`(the identifier of the clips collection where the ad will be displayed), `stClipCategories`(list of categories for the current clip associated with the item for ad targeting), `stNextClipCategories`(list of categories for the next clip associated with the item for ad targeting), `stAdIndex`(count of the Ad position from the start of the stCollection). | ### Bottom Banner Ads The Clips Player supports bottom banner ads rendered as standard banner views displayed below the video content. When using the GAM module, supply `bottomBannerAdUnit` in the `init()` method to fetch inline adaptive banners for the Clips bottom banner placement. Only 300x50 and 320x50 ad sizes are supported. Example setup with bottom banner ads: ```kotlin val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/ad_unit_id" }, bottomBannerAdUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/bottom_banner_ad_unit_id" }, ) } ``` #### StorytellerAdRequestInfo The `StorytellerAdRequestInfo` sealed class has two subclasses: **`ClipsAdRequestInfo`** Used when an Ad is requested for display in a Clips Player. The properties include: + `collection` - The identifier of the clips collection where the ad will be displayed + `nextClipCategories` - List of categories for the next clip associated with the item for ad targeting + `adIndex` - Count of the Ad position from the start of the Collection + `itemInfo` - Metadata about the item context for ad targeting **`StoriesAdRequestInfo`** Used when an Ad is requested for display in a Stories Player. The properties include: + `placement` - The identifier of the stories placement where the ad will be displayed + `categories` - List of category identifiers for ad targeting and filtering + `adIndex` - Count of the Ad position from the start of the Collection + `itemInfo` - Metadata about the item context for ad targeting #### ItemInfo Each request class includes an `ItemInfo` object that contains: + `categories` - List of categories associated with the item for ad targeting ## Non Skippable Ads Our Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS. ## AI notes / pitfalls / constraints - First-party ads require no integration changes; configuration is in CMS. - GAM setup requires coordination with the Storyteller Delivery team. - Bottom banner ads only support 300x50 and 320x50 sizes. - bottomBannerAdUnit can be null if you do not serve bottom banner ads. --- # Privacy and Tracking Source: public-docs/PrivacyAndTracking.md ## Summary - StorytellerEventTrackingOptions controls personalization and tracking at initialization. - Options cover storyteller tracking, user activity tracking, ads tracking, full video analytics, and remote viewing store. - disabledFunctionalFeatures disables specific functional areas and alters UI behavior. - Legacy enableEventTracking/disableEventTracking is deprecated. The `eventTrackingOptions` parameter customizes Storyteller's analytics and tracking behavior during SDK initialization. It allows certain features to be disabled based on user privacy choices. It is an object of type `StorytellerEventTrackingOptions` and by default, all of its properties are enabled: ```kotlin data class StorytellerEventTrackingOptions( val enablePersonalization: Boolean = true, val enableStorytellerTracking: Boolean = true, val enableUserActivityTracking: Boolean = true, val enableAdTracking: Boolean = true, val enableFullVideoAnalytics: Boolean = true, val enableRemoteViewingStore: Boolean = true ) // Set during initialization Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions(/*your setup here*/) ) // Read current settings (read-only) val currentOptions = Storyteller.eventTrackingOptions ``` > **Note:** Starting from version 11.0.0, `eventTrackingOptions` can only be set during SDK initialization and cannot be modified afterwards. To change these settings, you must call `Storyteller.initialize()` again with new options. ## Showcase examples - [Compose — privacy toggles (`AccountViewModel`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/account/AccountViewModel.kt#L83) - [Compose — stored tracking preferences (`SessionRepositoryImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/SessionRepositoryImpl.kt#L51) - [XML — re-initialize with `eventTrackingOptions` (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L95) ## User Personalization When `enablePersonalization` is enabled, user attributes and the user's ID are included on requests to Storyteller's servers to allow us to personalize the content returned ## Storyteller tracking When `enableStorytellerTracking` is enabled, we will record analytics events on our servers. *Note* that some events are necessary for user functionality and will still be transmitted (but not stored) even when this setting is off. ## User Activity tracking When `enableUserActivityTracking` is enabled, we will call the Storyteller delegate's method `onUserActivityOccurred()`, which allows integrating apps to record our analytics events on their own systems. ## Ads tracking When `enableAdTracking` is disabled, ad-related events will not be tracked through `onUserActivityOccurred()` Storyteller delegate method and on our servers. Additionally, only necessary fields like Ad Unit Id and Custom Template Id's will be included in GAM requests. ## Full Video Analytics When `enableFullVideoAnalytics` is enabled, detailed video information (story titles, clip titles, story IDs, clip IDs, etc.) is included in analytics events sent to the `onUserActivityOccurred()` delegate method. When disabled, sensitive video data fields are nullified for VPPA (Video Privacy Protection Act) compliance: - `storyId`, `storyTitle`, `storyDisplayTitle` - `clipId`, `clipTitle` - `pageId`, `pageTitle`, `itemTitle`, `containerTitle` - `cardId`, `cardTitle`, `cardSubtitle` This setting allows integrators to comply with video privacy regulations while still receiving engagement analytics events. All other analytics data (user interactions, durations, event types, etc.) continues to be provided. ## Remote Viewing Store When `enableRemoteViewingStore` is enabled (default), the SDK operates normally with full user activity tracking and remote storage capabilities. When `enableRemoteViewingStore` is disabled, the SDK operates in a privacy-enhanced mode designed to address VPPA (Video Privacy Protection Act) compliance concerns: - **No User ID Storage**: User IDs are never stored locally or sent to backend services - **Local-Only User Activity**: All user viewing activity is stored locally on the device and never synchronized with remote servers - **No Remote User Activity Fetch**: The SDK will not attempt to fetch user activity data from remote servers This mode is particularly useful for clients who need enhanced privacy protection while still maintaining local user experience features like viewing history and recommendations. Example for enhanced VPPA compliance: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true, enableUserActivityTracking = true, enableAdTracking = true, enableFullVideoAnalytics = false, // Nullifies sensitive video data enableRemoteViewingStore = false // Disables remote user ID storage and tracking ) ) ``` ## Disabled Functional Features The `disabledFunctionalFeatures` property allows you to selectively disable specific functional areas of the SDK while maintaining others. This provides granular control over which user interactions are tracked and stored. When a functional feature is disabled: - The SDK will not make API requests for that particular item (even if `enableRemoteViewingStore` is true) - The SDK will not store the relevant activity locally on device - The UI will behave as if the feature is disabled from the server Available functional features that can be disabled: - **All** - Disables everything including any future cases - **PageReadStatus** - Disables Page Read/Unread feature - **ClipViewedStatus** - Disables Clips Viewed/Not Viewed feature - **ClipLikes** - Disables Clip Likes and Unlikes - **ClipShares** - Disables Clip Share tracking and storage (but not the act of sharing) ### UI Behavior When Features Are Disabled **Pages**: When PageReadStatus is disabled, all rows act as if Read/Unread tracking is disabled from the server. **Clips**: When ClipViewedStatus is disabled, all entry points act as if Viewed/Not Viewed tracking is disabled from the server. **Clip Likes**: When disabled, users can like/unlike clips with UI feedback, but if they swipe away and return, the clip will appear unliked again. **Clip Shares**: When disabled, users can share clips and see count updates, but if they swipe away and return, the original count will be displayed. Example configuration: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true, enableUserActivityTracking = true, enableAdTracking = true, enableFullVideoAnalytics = true, enableRemoteViewingStore = true, disabledFunctionalFeatures = listOf( StorytellerDisabledFunctionalFeature.ClipLikes, StorytellerDisabledFunctionalFeature.ClipViewedStatus ) ) ) ``` ## enableEventTracking/disableEventTracking (Legacy) > Note: This method will be removed in version 11.0.0. Please use `eventTrackingOptions` instead. The `disableEventTracking()` method will disable storing analytics events on Storyteller servers. By default, event tracking is enabled. Note that some events are necessary for user functionality and will still be made (but not stored) when event tracking is disabled. Event tracking can be enabled again by calling `enableEventTracking()` Example: ```kotlin Storyteller.enableEventTracking() ``` ```kotlin Storyteller.disableEventTracking() ``` ## AI notes / pitfalls / constraints - eventTrackingOptions can only be set during initialization (11.0.0+). - Disabling full video analytics nullifies sensitive story and clip fields. - Disabling remote viewing store keeps activity local and avoids remote fetch. - disabledFunctionalFeatures affects likes, views, and share state in the UI. --- # Deep Linking Source: public-docs/DeepLinking.md ## Summary - Configure Android intent filters for Storyteller https hosts and custom scheme. - Use Storyteller.openDeepLink to open story, page, category, clip collection, or sheet. - openDeepLink parses URL patterns and routes to openStory/openPage/openCategory/openCollection/openSheet. - Digital Asset Links are required for https deep links on Android 6.0+. The Storyteller SDK makes it possible to handle the deep links. The implementing app should follow the official [Android guideline](https://developer.android.com/training/app-links/deep-linking). ## Showcase examples - [Compose — deep link handling (`DeeplinkHandler`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/DeeplinkHandler.kt#L39) - [XML — deep link handling (`MainActivity`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/MainActivity.kt#L43) In order to enable it separate intent filters need to be added to the implementing app's `AndroidManifest.xml`. ```xml ``` In the above, `[tenant_name]` is the lower case variant of your tenant's name. For example, if the tenant name is "MyTenant", then `[tenant_name]` should be "mytenant". Each link should be added as a separate intent-filter as in the example above. If Android system starting from Android 11 will fail to auto-verify any of the links which should be auto-verified it will disable the verification for all of them. This will result in the app not being able to handle any of the deep links in the Manifest. > Note: Please note that the last intent filter above for "[tenant_name]stories" does not have android:autoVerify="true" attribute. This is because the auto-verification is not supported for custom non https schemes. The deep link can be handled by using `Storyteller.openDeepLink` static method which opens the link. This can be done automatically by using `openDeepLink` or manually using Story or Page ID (`openStory` and `openPage` respectively). Example 1: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) val data: Uri? = intent?.data if (data != null && Storyteller.isStorytellerDeepLink(data.toString()) { //open link automatically Storyteller.openDeepLink(this, data.toString()) } } ``` > Note: Starting from Android 6.0+, a Digital Asset Links file is required to properly handle `https` deep links. It can be generated inside the Storyteller CMS and > it requires you to add your app's package name and SHA-256 fingerprint of certificate the app was signed with. For more information on the Digital Asset Links file, see the [Android developer documentation](https://developer.android.com/studio/write/app-link-indexing#associatesite) To generate the Digital Asset Links file in the Storyteller CMS: 1. Navigate to the 'Apps' section on the left-hand side 2. Click on the 'New App' button 3. Fill out the 'Package Name' and 'SHA 256 Cert Fingerprint' - Package Name - this is the application ID declared in the app's `build.gradle` file. This value should be the same as the "App Store Identifier" entered above. E.g. `com.example.myapp.` - SHA 256 Cert Fingerprint - this is the SHA256 fingerprint of your app's signing certificate. You can use the following command to generate the fingerprint via the Java keytool: `keytool -list -v -keystore my-release-key.keystore`. Or, if using the Google Play Store Signing, the SHA256 signature can be downloaded using the Google Play Console: + Login to the Play Console + Select the app you want to configure + Go to Setup → App integrity + Copy the SHA-256 value under "App signing certificate" + This value should be entered into your Storyteller App's form without the "SHA-256: " prefix. E.g.`15:8D:E9:83:C5:73...` After saving your app, the Digital Asset Links file for your tenant can be viewed at the following URLs: `https://yourtenantname.ope.nstori.es/assetlinks.json` and `https://yourtenantname.ope.nstori.es/.well-known/assetlinks.json` **Note: the file may take up to 5 minutes to appear after saving your App.** ## Deep Link Handling ### Open Deep Link The `openDeepLink` function in the Storyteller SDK handles different types of deep links. There are 3 different ways in which it does this ```kotlin fun openDeepLink(activity: Activity, url: String, onError: ((StorytellerError) -> Unit)) ``` This call makes Storyteller open the provided deeplink (showing the requested Page/Story). The method returns `true` if the URL is a Storyteller deeplink Parameters: - `activity` - Activity. - `url` - this is the Deep Link URL. - `onError` - this is called when there is any issue with opening the deep link (e.g. the requested content is no longer available) #### Story Category - For story category deep links, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/category/` or `/go/category/` - The link includes a category identifier following the `/category/` part of the path - The category ID is extracted from the deep link and the `openCategory` function is called with the extracted category ID - eg: `"https://[tenantname].shar.estori.es/go/category/123456"` or `"https://[tenantname].shar.estori.es/open/category/123456"` #### Clip collection - For clip collection deep links, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/clip`, `/go/clip`, `/open/clips`, or `/go/clips` - The link includes a unique identifier (UUID) in the path - The link must include a `collectionId` query parameter - An optional `categoryId` query parameter can be included to specify a category to open in the Clip Collection as an initial category (`initialCategory` of the openCollection method) - The information extracted will be used to call `openCollection()` - eg: `"https://[tenantname].shar.estori.es/open/clip/((clipId))?collectionId=abcd1234?categoryId=123456"` #### Story Deep link - For story deep links, the openDeepLink method identifies and processes links based on the following structure: - The link is confirmed to be a URI object - It extracts storyId or pageId from the deep link via segment positions. One must be present - It proceeds to call `openPage()` or `openStory()` with either the storyId or pageId, - eg openStory: `https://[tenantname].shar.estori.es/go/story/((storyId))` or openPage: `https://[tenantname].shar.estori.es/go/page/((pageId))` #### Sheet - For sheet deep link, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/sheet/` or `/go/sheet/` - It extracts `sheetId` from the deep link via segment positions. One must be present - eg: `"https://[tenantname].shar.estori.es/go/sheet/123456"` or `"https://[tenantname].ope.nstori.es/open/sheet/123456"` ## Manual Deep Link Handling It is generally recommended to author your own deeplink and extract the required data. This is to not upset the current state and deeplink integration of your app and to allow for more control over the deeplink handling then use `openStory()`/`openPage()`/`openCollection()`/`openCategory()`/`openSheet()` to pass the relevant data to the SDK, see the Open Player (or AdditionalMethods) documentation for more information. ### isStorytellerDeepLink `isStorytellerDeepLink(String)` checks if the string is a valid Storyteller deep link. ```kotlin Storyteller.isStorytellerDeepLink("stories://open/9329bed2-2311-69b8-cbcf-39fcf8d8af21/f6445df7-bd79-71de-cdfb-39fd071568a1") ``` ## AI notes / pitfalls / constraints - Each intent filter should be separate; failed auto-verify can disable all. - Custom scheme links do not support autoVerify. - openDeepLink returns false if the URL is not a Storyteller deep link. - Tenant name in hosts and scheme must be lowercase. ## Related topics - Open Player - AdditionalMethods --- # Open Player Source: public-docs/OpenPlayer.md ## Summary - Use openStory/openPage to open a specific story or page by ID. - Use openCollection or openCategory to open clips collections or story categories. - External ID helpers openStoryByExternalId and openCollectionByExternalId. - All open methods accept an Activity and optional onError callback. ## Showcase examples - [Compose — opening a category (`ImageActionItem`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/ImageActionItem.kt#L60) ## Open Stories This call opens a Story with a given ID. The call will only open that individual Story. ```kotlin fun openStory(activity: Activity, storyId: String? = null, onError: (StorytellerError) -> Unit = {}) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `storyId` - this is a Story's ID, if it is `null` then `onError` will be called - `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available) ## Open Pages This call opens a Page with a given ID. The call will only open that individual Story containing that page. ```kotlin fun openPage(activity: Activity, pageId: String? = null, onError: (StorytellerError) -> Unit = {}) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `pageId` - this is a Page's ID, if it is `null` then `onError` will be called - `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available) ## Open Collection ```kotlin fun openCollection( activity: Activity, configuration: StorytellerClipCollectionConfiguration, titleDrawable: Drawable? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `configuration` - this is a Clip Collection configuration, see below - `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional) - `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available) `StorytellerClipCollectionConfiguration` parameters: - `collectionId` - this is a Clip Collection's ID (required) - `categoryId` - this is a Category ID (optional), if provided, this category will be opened, if the category is not available or not found it will be ignored - `clipId` - this is a Clip ID to open (optional) ## Open Category This call opens a Story category. If Story ID is not supplied, the first Story in the collection will be opened. ```kotlin fun openCategory( activity: Activity, category: String, storyId: String? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `category` - this is a Story category - `storyId` - this is a Story ID (optional) - `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available) ### openStoryByExternalId This call opens a Story by external ID. ```kotlin fun openStoryByExternalId( context: Activity, externalId: String? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `externalId` - external Id to open a Story - `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available) ### openCollectionByExternalId This call opens a Collection of Clips and shows Clip with external id. If Clip ID is not supplied, the first Clip in the collection will be opened. ```kotlin fun openCollectionByExternalId( activity: Activity, collectionId: String, externalId: String? = null, titleDrawable: Drawable? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `collectionId` - this is a Clip Collection's ID - `externalId` - this is a Clip's external ID (optional) - `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional) - `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available) ## AI notes / pitfalls / constraints - onError is called when IDs are null or content is unavailable. - openCollection uses StorytellerClipCollectionConfiguration; categoryId is optional. - openCollectionByExternalId opens the first clip if externalId is not provided. --- # Search Source: public-docs/Search.md ## Summary - Search UI supports stories and clips with suggestions and result categories. - Filters include date posted, content type, and sort order. - enableSearch in CMS controls availability; openSearch can launch search from anywhere. The `Search` component allows users to search for Storyteller clips and stories. As users type, a list of suggestions will appear, from which users can either select a suggestion or search using their entered term. Results are categorized into two sections: `Stories` and `Clips`, based on their type. Filters allow users to narrow down their search results, enabling them to find specific content that meets their criteria more efficiently. For instance, users can apply filters such as date posted, content type or they can sort it by certain criteria. ## Showcase examples - [Compose — opening Search (`MainScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/main/MainScreen.kt#L75) ## Search Filters `Date Posted` possible values: - `All` - default value - `Past 24 hours` - `Last Week` - `Last Month` - `Last Year` `Content Type` possible values: - `All` - default value - `Stories` - `Clips` `Sort By` values: - `Relevance` - default value - `Like Count` - `Share Count` - `Date Posted` ## How to Use The `Search` feature can be enabled through CMS by setting the `enableSearch` value to `true`. Once enabled, the Search functionality will be available in both Clip and Story players without any additional changes required from the integrating app. Additional `Search` functionality is available through the `Storyteller` class: - `isSearchEnabled` - returns whether search functionality is enabled at the app level. - `openSearch` - opens the `Search`component from anywhere in the app. If Storyteller player is currently displayed, it will be dismissed before presenting the `Search` component. ## Customization Certain UI parts of the `Search` component can be customized. For more information, see Themes. ## AI notes / pitfalls / constraints - openSearch dismisses the player if it is currently visible. - isSearchEnabled reflects app-level configuration. - Search theming is controlled via Themes. ## Related topics - Themes --- # Storyteller Brightcove Module Source: public-docs/StorytellerBrightcove.md ## Summary - Adds Brightcove analytics support as a separate module. - Install the dependency and register the module via Storyteller.modules. - Requires SDK version 10.6.3 or higher. ## Introduction The Storyteller SDK supports Brightcove analytics as a separate module. This module is designed to work with the Brightcove to provide analytics for videos. ### Setup To setup the module additional dependency needs to be added and the module needs to be initialized and included in `Storyteller.modules` ```groovy maven { url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk") } ``` Then add the following reference to your version catalog file: ```toml storyteller-brightcove = { module = "com.storyteller.brightcove:analytics", version.ref = "1.2.0" } ``` And finally reference this in your `build.gradle`: ```groovy implementation(libs.storyteller.brigthcove) ``` To use the module Storyteller SDK version should be set to `10.6.3` or higher. Now initialize the module as follows: ```kotlin val configuration = StorytellerBrightcoveModuleConfiguration( accountId = "your-account-id", playerName = "your-player-name", source = "your-source", destination = "your-destination-url", ) val storytellerBrigthcoveModule = StorytellerBrightCoveModule(configuration) fun initializeStoryteller() { Storyteller.modules = listOf(storytellerBrigthcoveModule) //initialize code } ``` ## Showcase examples - [Compose — Brightcove module config (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L83) ## AI notes / pitfalls / constraints - Module configuration requires accountId, playerName, source, and destination URL. - Include the module before initialization. --- # Storyteller Cards Source: public-docs/Cards.md ## Summary - Storyteller Cards are CMS-configured components that open stories, clips, or custom actions. - Use StorytellerCard composable with StorytellerCardDataModel and optional delegate. - StorytellerCardState supports reloadData for manual refresh. - Card appearance and button behavior are configured via CMS and theme properties. Storyteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title and subtitle. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user. ## Showcase examples - [Compose — rendering Cards (`HomeScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/HomeScreen.kt#L202) ## Usage You can integrate Storyteller Cards into your app using Jetpack Compose. ### Jetpack Compose For Jetpack Compose, use the `StorytellerCard` composable. 1. **Data Model**: Create a `StorytellerCardDataModel` object, specifying the `collectionId` for the Card collection you want to display. You can also provide optional `context` data for analytics. When configured, `context` will be included in all analytics events when users interact with the Card. See Analytics for more details. 2. **State**: Initialize a `StorytellerCardState` using `rememberStorytellerCardState()` to manage the card's state and reload functionality. 3. **View**: Create the `StorytellerCard` composable, passing in the `StorytellerCardDataModel` instance and state. 4. **Delegate (Optional)**: Provide an optional `StorytellerCardViewDelegate` to handle events like `onDataLoadComplete`. This allows you to react to data loading success or failure (e.g., by hiding the component). ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.storyteller.ui.compose.components.cards.StorytellerCard import com.storyteller.ui.compose.components.cards.StorytellerCardDataModel import com.storyteller.ui.compose.components.cards.StorytellerCardViewDelegate import com.storyteller.ui.compose.components.cards.rememberStorytellerCardState import com.storyteller.domain.entities.StorytellerError @Composable fun MyCardSection() { val dataModel = StorytellerCardDataModel( collectionId = "card-collection-id", context = hashMapOf("placementId" to "home_card", "location" to "Home") ) val cardState = rememberStorytellerCardState(dataModel.collectionId) val cardViewDelegate = remember { object : StorytellerCardViewDelegate { override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (success) { println("Card data loaded successfully!") } else { println("Card data failed to load: ${error?.message}") } } } } StorytellerCard( dataModel = dataModel, state = cardState, delegate = cardViewDelegate, modifier = Modifier ) // Optional: Add a reload button Button(onClick = { cardState.reloadData() }) { Text("Reload Card") } } ``` ## Reloading The `StorytellerCardState` provides a `reloadData()` method. Call this method to manually trigger a refresh of the Card data from the server. ## Viewed/Tapped Ordering In the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content. ## Theming Card appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection. The following properties can be configured in the CMS and influence the Card's presentation and behaviour: - `style.textLengthMode` (default: `truncate`): How text that exceeds the available space is handled. +`truncate`: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit. +`resize`: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading). - `style.textAlignment` (default: `start`): Horizontal alignment of the heading and subheading. Can be `start`, `center`, or `end`. - `style.padding` (default: `12`): Inner padding around the text content. + For full-bleed cards ( `marginHorizontal = 0`) with text _below_ the image and _all_ cards with text _on_ the image, padding is applied to all sides of the text. + For cards with text _below_ the image where `marginHorizontal > 0`, padding is applied only to the top and bottom of the text. - `style.marginHorizontal` (default: `0`): Horizontal margin around the card. `0` means full-bleed. - `style.cornerRadius` (default: `{theme.primitives.cornerRadius}`): Corner radius of the card. The application depends on `marginHorizontal` and text position. + Not applied for full-bleed cards (`marginHorizontal=0`) with text _below_ the image. + Applied to the _image_ for cards with text _below_ the image and `marginHorizontal > 0`. + Applied to the _whole card_ for cards with text _on_ the image and `marginHorizontal > 0`. - `style.heading.font` (default: `{theme.customFont}`): Font family for the heading. - `style.heading.textSize` (default: `22`): Font size for the heading. - `style.heading.lineHeight` (default: `28`): Line height for the heading. - `style.heading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.heading.letterSpacing` (default: `0`): Letter spacing for the heading. - `style.heading.textColor` (default: `{theme.colors.white.primary}`): Text color for the heading. The default applies when text is displayed _on_ the background asset. When text is displayed _below_ the background, the default color is `{theme.colors.black.primary}` in light mode and `{theme.colors.white.primary}` in dark mode. - `style.subHeading.font` (default: `{theme.customFont}`): Font family for the subheading. - `style.subHeading.textSize` (default: `16`): Font size for the subheading. - `style.subHeading.lineHeight` (default: `21`): Line height for the subheading. - `style.subHeading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.subHeading.letterSpacing` (default: `0`): Letter spacing for the subheading. - `style.subHeading.textColor` (default: `{theme.colors.white.secondary}`): Text color for the subheading. The default applies when text is displayed _on_ the background asset. When text is displayed _below_ the background, the default color is `{theme.colors.black.secondary}` in light mode and `{theme.colors.white.secondary}` in dark mode. - `behavior.reloading.reloadOnExit` (default: `true`): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player). - `behavior.reloading.reloadOnForeground` (default: `true`): Whether the Card Collection reloads when the app comes to the foreground. ### Button Behavior - **Button positioning**: Buttons are optional visual elements that follow the `textOverContent` property: + When `textOverContent = true`: Button appears on the card (overlaying the content), positioned below the title/subtitle + When `textOverContent = false`: Button appears below the card (below the title/subtitle section) - **Button functionality**: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped - **Button text**: The button text is defined in the Card data, not the theme ### Button Theme Properties - `style.button.title.font` (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height. - `style.button.title.textSize` (default: `16`): Font size for the button text. - `style.button.title.lineHeight` (default: `21`): Line height for the button text. - `style.button.title.textCase` (default: `default`): Text case transformation for the button text (`upper`, `lower`, `default`). - `style.button.title.letterSpacing` (default: `0`): Letter spacing for the button text. - `style.button.title.textColor` (default: `{theme.colors.white.primary}` for text on image, `{theme.colors.black.primary}` for text below image in light mode, `{theme.colors.white.primary}` for text below image in dark mode): Text color for the button. If not specified or null, uses the same color logic as the outline color. - `style.button.backgroundColor` (optional): Background color of the button. If not set, the button will have a transparent background with an outline. - `style.button.outlineColor` (default: `{theme.colors.white.primary}` for text on image, `{theme.colors.black.primary}` for text below image in light mode, `{theme.colors.white.primary}` for text below image in dark mode): Color of the button outline/border. - `style.button.outlineWidth` (default: `1`): Width of the button outline/border in points. - `style.button.cornerRadius` (optional, default: `{theme.primitives.cornerRadius}`): Corner radius of the button. If null or not set, falls back to the theme's default corner radius. - `style.button.textAlignment` (default: uses card `textAlignment`): Text alignment for the button text. If not specified or null, uses the card's text alignment setting. ## AI notes / pitfalls / constraints - collectionId is required in StorytellerCardDataModel. - reloadData refreshes card content from the server. - Button taps do not change the card action; the entire card remains tappable. - Context data is included in analytics events when provided. ## Related topics - Analytics --- # Open Source Licenses Source: public-docs/OpenSourceLicenses.md ## Summary - Lists third-party libraries used by the SDK. - Libraries are licensed under Apache 2.0. - Full license text is included for attribution. The Storyteller SDK uses open source libraries to accomplish some of its functionality. Those libraries are: - [Retrofit](https://github.com/square/retrofit) - [ExoPlayer](https://github.com/google/ExoPlayer) - [Coil](https://coil-kt.github.io/coil/) - [OkHttp](https://square.github.io/okhttp/) These libraries are all subject to the [Apache 2.0](https://github.com/square/retrofit/blob/master/LICENSE.txt) open source license. According to the terms of the license, it may be necessary to include a statement of this license somewhere in the app which integrates the SDK. You should consult your own legal adviser if you are unsure. The text of the license is included below: ```text Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ``` ## AI notes / pitfalls / constraints - You may need to include Apache 2.0 attribution in your app. - Consult legal guidance if you are unsure about compliance.