# Android Integrate the Airship SDK into your mobile applications for Android, Android TV, FireOS, and FireTV. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#handling-display-requests) and [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/android/preference-center/embedding/#handling-display-requests). #### Kotlin Set the deep link listener during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/): ```kotlin Airship.deepLinkListener = DeepLinkListener { deepLink: String -> // Handle deep link true } ``` #### Java Set the deep link listener during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/): ```java Airship.setDeepLinkListener(deepLink -> { // Handle the deepLink return true; }); ``` # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. The SDK provides asynchronous access to feature flags using Kotlin suspend functions, which is intended to be called from a coroutine. For more information, see [Coroutines Overview guide](https://kotlinlang.org/docs/coroutines-overview.html). #### Kotlin ```kotlin // Get the FeatureFlag result val result: Result = FeatureFlagManager.shared().flag("YOUR_FLAG_NAME") // Check if the app is eligible or not if (result.getOrNull()?.isEligible == true) { // Do something with the flag } else { // Disable feature or use default behavior } ``` #### Java ```java // Get the FeatureFlag FeatureFlag featureFlag = FeatureFlagManager.shared().flagAsPendingResult("YOUR_FLAG_NAME").getResult(); // Check if the app is eligible or not if (featureFlag != null && featureFlag.isEligible()) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). #### Kotlin ```kotlin FeatureFlagManager.shared().trackInteraction(featureFlag) ``` #### Java ```java FeatureFlagManager.shared().trackInteraction(featureFlag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. #### Kotlin ```kotlin FeatureFlagManager.shared().flag("YOUR_FLAG_NAME").fold( onSuccess = { flag -> // do something with the flag }, onFailure = { error -> // do something with the error } ) ``` #### Java ```java FeatureFlag featureFlag = FeatureFlagManager.shared().flagAsPendingResult("YOUR_FLAG_NAME").getResult(); if (featureFlag == null) { // error } else if (featureFlag.isEligible()) { // Do something with the flag } ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Action Situations Actions are triggered with extra context in the form of a Situation. The different situations allow actions to determine if they should run, and may perform different behavior depending on the situation. | Description | Android | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| | Action was invoked manually. | [.SITUATION_MANUAL_INVOCATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-m-a-n-u-a-l_-i-n-v-o-c-a-t-i-o-n/index.html) | | Action was invoked from a launched push notification. | [.SITUATION_PUSH_OPENED](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-p-u-s-h_-o-p-e-n-e-d/index.html) | | Action is triggered when a notification is opened. | [.SITUATION_PUSH_OPENED](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-p-u-s-h_-o-p-e-n-e-d/index.html) | | Action was invoked from JavaScript or a URL. | [.SITUATION_WEB_VIEW_INVOCATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-w-e-b_-v-i-e-w_-i-n-v-o-c-a-t-i-o-n/index.html) | | Action was invoked from a foreground interactive notification button. | [.SITUATION_FOREGROUND_NOTIFICATION_ACTION_BUTTON](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-f-o-r-e-g-r-o-u-n-d_-n-o-t-i-f-i-c-a-t-i-o-n_-a-c-t-i-o-n_-b-u-t-t-o-n/index.html) | | Action was invoked from a background interactive notification button. | [.SITUATION_BACKGROUND_NOTIFICATION_ACTION_BUTTON](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-b-a-c-k-g-r-o-u-n-d_-n-o-t-i-f-i-c-a-t-i-o-n_-a-c-t-i-o-n_-b-u-t-t-o-n/index.html) | | Action was invoked from automation. | [.SITUATION_AUTOMATION](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.actions/-action/-situation/-a-u-t-o-m-a-t-i-o-n/index.html) | ## Action Registry The action registry is the central place to register actions by name. Each entry in the registry contains an action, the names that the action is registered under, a predicate that allows filtering when an action should run, and allows specifying alternative actions for different situations. #### Kotlin ```kotlin Airship.actionRegistry.registerEntry(setOf("my_action_name", "my_alias")) { ActionRegistry.Entry(action = CustomAction()) } ``` #### Java ```java Airship.getActionRegistry().registerEntry(Set.of("my_action_name", "my_alias"), () -> { return new ActionRegistry.Entry(new CustomAction()); }); ``` #### Kotlin ```kotlin val entry = Airship.actionRegistry.getEntry("my_action_name") ``` #### Java ```java ActionRegistry.Entry entry = Airship.getActionRegistry().getEntry("my_action_name"); ``` #### Kotlin ```kotlin // Predicate that will reject PUSH_RECEIVED, causing the action to never run during that situation. val rejectPushReceivedPredicate: ActionPredicate = object : ActionPredicate { override fun apply(arguments: ActionArguments): Boolean { return SITUATION_PUSH_RECEIVED != arguments.situation } } // Update the entry with a new predicate Airship.actionRegistry.updateEntry("my_action_name", predicate = rejectPushReceivedPredicate) ``` #### Java ```java // Predicate that will reject PUSH_RECEIVED, causing the action to never run during that situation. ActionPredicate rejectPushReceivedPredicate = new ActionPredicate() { @Override public boolean apply(ActionArguments arguments) { return !(SITUATION_PUSH_RECEIVED.equals(arguments.getSituation())); } }; // Update the entry with a new predicate Airship.getActionRegistry().updateEntry("my_action_name", null, Collections.emptyMap(), rejectPushReceivedPredicate); ``` ## Triggering Actions In addition to triggering actions from messages, you can trigger them programmatically. #### Kotlin ```kotlin // Running an action directly through the ActionRunRequest ActionRunRequest.createRequest("actionName") .setSituation(SITUATION_MANUAL_INVOCATION) .setValue("actionValue") .run() // Running an action by registered name ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run() // An optional callback when finished ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run { arguments, result -> Logger.info("Action finished! Result: $result") } // Block until the action finishes val result = ActionRunRequest.createRequest("my_action_name").runSync() ``` #### Java ```java // Running an action directly through the ActionRunRequest ActionRunRequest.createRequest("actionName") .setSituation(Situation.MANUAL_INVOCATION) .setValue("actionValue") .run(); // Running an action by registered name ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run(); // An optional callback when finished ActionRunRequest.createRequest("my_action_name") .setValue("actionValue") .run(new ActionCompletionCallback() { public void onFinish(ActionArguments arguments, ActionResult result) { Logger.info("Action finished! Result: " + result); } }); // Block until the action finishes ActionResult result = ActionRunRequest.createRequest("my_action_name").runSync(); ``` ## Custom Actions The action framework supports any custom actions. Create an action by extending the `Action` base class on Android. After `takeOff`, register the action. The action can be triggered the same way as built-in actions. #### Kotlin ```kotlin class CustomAction : Action() { override fun acceptsArguments(arguments: ActionArguments): Boolean { if (!super.acceptsArguments(arguments)) { return false } // Do any argument inspections. The action will stop // execution if this method returns false. return true } override fun perform(arguments: ActionArguments): ActionResult { Log.i("CustomAction", "Action is performing!") return ActionResult.newEmptyResult() } } ``` #### Java ```java public class CustomAction extends Action { @Override public boolean acceptsArguments(ActionArguments arguments) { if (!super.acceptsArguments(arguments)) { return false; } // Do any argument inspections. The action will stop // execution if this method returns false. return true; } @Override public ActionResult perform(ActionArguments arguments) { Log.i("CustomAction", "Action is performing!"); return ActionResult.newEmptyResult(); } } ``` > **Note:** On Android, custom actions may override the `shouldRunOnMainThread()` method to specify whether the action should > be run on the main tread, or on a background thread. Implementations should take care to avoid long-running tasks, > especially when running on the main thread. # Live Updates > Integrate Live Updates into your Android app to display real-time updates in notifications, widgets, or within your app. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ## Installation Include the Live Updates module in your app's dependencies: #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-live-update:$airshipVersion") } ``` #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" implementation "com.urbanairship.android:urbanairship-live-update:$airshipVersion" } ``` ## Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: #### Kotlin ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` #### Java ```java public class SampleLiveUpdateHandler extends CallbackLiveUpdateNotificationHandler() { @Override public void onUpdate( Context context, LiveUpdateEvent event, LiveUpdate update, LiveUpdateResultCallback callback ) { // Read content_state fields from the Live Update payload int teamOneScore = update.getContent().optInt("team_one_score", 0); int teamTwoScore = update.getContent().optInt("team_two_score", 0); String statusUpdate = update.getContent().optString("status_update"); // Expanded notification layout RemoteViews bigLayout = new RemoteViews(context.getPackageName(), R.layout.sports_big); bigLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); bigLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); bigLayout.setTextViewText(R.id.statusUpdate, statusUpdate); // Collapsed notification layout RemoteViews smallLayout = new RemoteViews(context.getPackageName(), R.layout.sports_small); smallLayout.setTextViewText(R.id.teamOneScore, String.valueOf(teamOneScore)); smallLayout.setTextViewText(R.id.teamTwoScore, String.valueOf(teamTwoScore)); // Create the notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout); // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. callback.onResult(LiveUpdateResult.ok(builder)); } private static final String NOTIFICATION_CHANNEL_ID = "sports"; } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. In your `Autopilot` class, or in `Application.onCreate`, register the types. #### Kotlin ```kotlin class SampleAutopilot : Autopilot() { override fun onAirshipReady(context: Context) { Airship.liveUpdateManager.register( type = "notification", handler = SampleLiveUpdateHandler() ) } } ``` #### Java ```java public class SampleAutopilot extends Autopilot() { @Override public void onAirshipReady(Context context) { LiveUpdateManager.shared().register("notification", new SampleLiveUpdateHandler()); } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.start( name = "sports-game-123", type = "notification", content = jsonMapOf( "team_one_score" to 0, "team_two_score" to 0, "status_update" to "Game started!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 0); content.put("team_two_score", 0); content.put("status_update", "Game started!"); LiveUpdateManager.shared().start( "sports-game-123", "notification", content ); ``` ### Updating Live Updates Live Updates can be updated from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.update( name = "sports-game-123", content = jsonMapOf( "team_one_score" to 3, "team_two_score" to 0, "status_update" to "Game started!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 3); content.put("team_two_score", 0); content.put("status_update", "Game started!"); LiveUpdateManager.shared().update( "sports-game-123", content ); ``` ### Ending Live Updates You can end a Live Update from within the app. #### Kotlin ```kotlin Airship.liveUpdateManager.stop( name = "sports-game-123", content = jsonMapOf( "team_one_score" to 9, "team_two_score" to 6, "status_update" to "Game over!" ) ) ``` #### Java ```java Map content = new HashMap<>(); content.put("team_one_score", 9); content.put("team_two_score", 6); content.put("status_update", "Game over!"); LiveUpdateManager.shared().stop( "sports-game-123", content ); ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. #### Kotlin ```kotlin Airship.liveUpdateManager.clearAll() ``` #### Java ```java LiveUpdateManager.shared().clearAll(); ``` # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. Analytics allows you to track user engagement and app performance through custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with [Custom Events](https://www.airship.com/docs/reference/glossary/#custom_event). They require enabling analytics for your app. #### Kotlin ```kotlin customEvent("event_name") { addProperty("my_custom_property", "some custom value") addProperty("is_neat", true) addProperty("any_json", jsonMapOf("foo" to "bar")) }.track() ``` #### Java ```java CustomEvent.newBuilder("event_name") .setEventValue(123.12) .addProperty("my_custom_property", "some custom value") .addProperty("is_neat", true) .addProperty("any_json", JsonMap.newBuilder() .put("foo", "bar") .build()) .build() .track(); ``` ### Templates Custom Event Templates are a wrapper for Custom Events and are available for Android, [iOS](https://www.airship.com/docs/developer/sdk-integration/apple/analytics/#templates), and [Web](https://www.airship.com/docs/developer/sdk-integration/web/analytics-and-reporting/#templates). See also [CustomEvent](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.analytics/-custom-event/index.html) in the Android SDK library. #### Account Use this template to create Custom Events for account-related events. The template is written with account registration as the example. #### Kotlin Track a registered account event: ```kotlin customEvent(AccountEventTemplate.Type.REGISTERED) { }.track() ``` With optional properties: ```kotlin customEvent( type = AccountEventTemplate.Type.REGISTERED, properties = AccountEventTemplate.Properties( category = "premium" ) ) { setEventValue(9.99) setTransactionId("12345") }.track() ``` #### Media Use this template to create Custom Events for media-related events, including consuming, browsing, starring, and sharing content. #### Kotlin Track a consumed content event: ```kotlin customEvent(MediaEventTemplate.Type.Consumed) { }.track() ``` With an optional value: ```kotlin customEvent(MediaEventTemplate.Type.Consumed) { setEventValue(1.99) }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Consumed, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { setEventValue(2.99) }.track() ``` #### Kotlin Track a starred content event: ```kotlin customEvent(MediaEventTemplate.Type.Starred) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Starred, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { setEventValue(2.99) }.track() ``` #### Kotlin Track a browsed content event: ```kotlin customEvent(MediaEventTemplate.Type.Browsed) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Browsed, properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", author = "UA Enterprises", isFeature = true, publishedDate = "August 25, 2016" ) ) { }.track() ``` #### Kotlin Track a shared content event: ```kotlin customEvent(MediaEventTemplate.Type.Shared()) { }.track() ``` With a source and medium: ```kotlin customEvent( MediaEventTemplate.Type.Shared(source = "facebook", medium = "social") ) { }.track() ``` With optional properties: ```kotlin customEvent( type = MediaEventTemplate.Type.Shared(source = "facebook", medium = "social"), properties = MediaEventTemplate.Properties( id = "12345", category = "entertainment", type = "video", eventDescription = "Watching latest entertainment news.", author = "UA Enterprises", isFeature = true, publishedDate = "August 24, 2016" ) ) { }.track() ``` #### Retail Use this template to create Custom Events for retail-related events, including browsing a product, adding an item to a cart, purchasing an item, starring a product, and sharing a product. #### Kotlin Track a purchased event: ```kotlin customEvent(RetailEventTemplate.Type.Purchased) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Purchased, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a browsed event: ```kotlin customEvent(RetailEventTemplate.Type.Browsed) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Browsed, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track an added-to-cart event: ```kotlin customEvent(RetailEventTemplate.Type.AddedToCart) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.AddedToCart, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a starred product event: ```kotlin customEvent(RetailEventTemplate.Type.Starred) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Starred, properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` #### Kotlin Track a shared product event: ```kotlin customEvent(RetailEventTemplate.Type.Shared()) { }.track() ``` With a source and medium: ```kotlin customEvent( RetailEventTemplate.Type.Shared(source = "facebook", medium = "social") ) { }.track() ``` With optional properties: ```kotlin customEvent( type = RetailEventTemplate.Type.Shared(source = "facebook", medium = "social"), properties = RetailEventTemplate.Properties( id = "12345", category = "mens shoes", eventDescription = "Low top", brand = "SpecialBrand", isNewItem = true ) ) { setEventValue(99.99) setTransactionId("13579") }.track() ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. #### Kotlin ```kotlin Airship.analytics.editAssociatedIdentifiers { addIdentifier("key", "value") } ``` #### Java ```java Airship.getAnalytics() .editAssociatedIdentifiers() .addIdentifier("key", "value") .apply(); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. #### Kotlin ```kotlin Airship.analytics.trackScreen("MainScreen") ``` #### Java ```java Airship.getAnalytics().trackScreen("MainScreen"); ``` # Android SDK Changelog > The latest updates to the Airship Android SDK. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 20.7.2 May 13, 2026 Patch release that fixes a couple of Scene issues and improves remote data refresh behavior for kiosk-style apps. Apps that make use of SMS inputs in Scenes should update to this version or newer. ### Changes - Fixed SMS text input in Scenes - Updated Scene label and label button to improve rendering consistency - Improved remote data refresh behavior for kiosk-style apps that stay foregrounded for long periods of time ## 20.7.1 May 8, 2026 Patch release that includes minor API changes to allow for email and SMS registration in Airship cross-platform frameworks. ### Changes - Internal API changes to support email and SMS registration in frameworks. ## 20.7.0 April 30, 2026 Minor release that adds support for Native Message Center. ### Changes - Added support for rendering Native Content in Message Center. ## 20.6.4 April 24, 2026 Patch release that fixes keyboard resize handling in modal scenes. ### Changes - Fixed modal scenes so content resizes correctly when the soft keyboard appears, closing a gap between content and the keyboard on older API levels ## 20.6.3 April 20, 2026 Patch release with several push reliability improvements. ### Changes - Fixed a race condition in `PushManager` that could cause false push opt-outs when FCM tokens rotate or registration fails transiently - Fixed `SQLiteBlobTooBigException` errors in `PreferenceDataStore` for large stored values - Fixed invalid JSON logging - Fixed unnecessary backoff when Airship's WorkManager jobs are cancelled externally ## 20.6.2 April 15, 2026 Patch release that hardens against a specific WebView crash that can occur on certain Android 16 devices. ### Changes - Avoid crashing if WebView inflation fails in `HtmlActivity`, which displays Custom HTML IAX messages, when we encounter a known issue on Android 16 that primarly impacts Samsung devices (https://issuetracker.google.com/issues/448359671) ## 20.6.1 March 27, 2026 Patch release that fixes a dependency resolution issue with the FCM module introduced in 20.4.0. Apps that depend on `urbanairship-fcm` and reference Firebase Messaging classes directly should update to this version. ### Changes - Fixed `firebase-messaging` dependency not being available on the compile classpath ## 20.6.0 March 25, 2026 Minor release that extends Markdown support in Scenes and improves handling of navigation to invalid Message Center message IDs. ### Changes - Added superscript and subscript Markdown support in Scenes (`^^superscript^^` and `,{subscript},`) - Updated Message Center to show the message view with an error when attempting to open a message with an invalid message ID, instead of failing to the Messages list ## 20.5.0 March 17, 2026 Minor release that improves video playback and pager navigation reliability in Scenes, along with several bug fixes. This release also includes updates to proguard rules to support behavior changes in AGP 9. Apps that have migrated to AGP 9.x should update to this version or newer. ### Changes - Improved video playback lifecycle handling in Scenes - Improvements for Scenes with complex branching - Fixed `Airship.takeOff` returning before `onReady` callbacks have completed - Fixed possible hang when calling `fetchMessages` on Message Center - Fixed Message Center inbox update notifications - Fixed Message Center message content type parsing - Fixed SMS validation error handling - Fixed overly frequent permission listener callbacks - Updated proguard rules to keep default constructors for Airship classes ## 20.4.0 March 4, 2026 Minor release with a pair of improvements for Scenes. ### Changes - Adjusted Markdown rendering in Scenes to be less aggressive when interpreting styling delimiters inside of words - Improved Scene border rendering when rounded corners are present ## 20.3.0 February 25, 2026 Minor release that adds support for Native Message Center. Native content type requires displaying the message content in an Airship Message View. Apps that do not use Airship's message views (e.g. using a WebView directly) should filter out messages where `message.contentType` is not `Message.ContentType.Html`. ### Changes - Removed library group restrictions on `PushProviderBridge`. - Added support for Native Message Center. ## 20.2.2 February 19, 2026 Patch release with an FCM availability check improvement to better handle unexpected Google Play service lookup failures. ### Changes - Added exception handling and logging around the FCM Google Play Store availability check to prevent unexpected crashes when Google checks fail. ## 20.2.1 February 6, 2026 Patch release with several minor improvements for the Compose Message Center UI. Apps that make use of the Compose Message Center should update to take advantage of these improvements. ### Changes - Allows the Compose Message Center toolbar title to be overridden via `MessageCenterOptions` - Option to disable message deletion in the Compose Message Center via `MessageCenterOptions` - Fixed the up arrow and `onNavigateUp` callback for the Compose Message Center list screen ## 19.13.8 January 28, 2026 Patch release that fixes an issue with custom events being double counted for IAX triggers (reporting was not affected). Apps that make use of custom event triggers in IAX should update to this version or later. ### Changes - Fixed issue that caused custom events being double counted for IAX triggers (reporting was not affected) ## 20.2.0 January 28, 2026 Minor release that adds a new `PreferenceCenterView`, fixes fetching subscription lists after changing contact IDs, and improvements for Scenes. ### Changes - Added `PreferenceCenterView` for easier integration of Preference Centers when the `Fragment` API is not desired - Fixed issue where subscription lists could remain cached after changing contact IDs - Improved measurement of videos inside of containers in Scenes - Improved checkbox and radio button accessibility in Scenes - Improved TalkBack navigation for Scenes with Pagers - Fixed issue that caused custom events being double counted for IAX triggers (reporting was not affected) ## 20.1.1 January 17, 2026 Patch release that fixes a potential image-related crash in Scenes and acessibility issues. ### Changes - Fixed a potential crash in Scenes with specific images and display settings - Fixed Message Center title not being marked as a heading - Fixed Scene icon buttons not having a proper disabled effect ## 19.13.7 January 16, 2026 Patch release that fixes a potential image-related crash in Scenes. ### Changes - Fixes a potential crash in Scenes with specific images and display settings. ## 20.1.0 January 9, 2026 Minor release that includes several fixes and improvements for Scenes, In-App Automations, and notification handling. ### Changes - Fixed a measurement issue with videos inside of containers in Scenes in certain configurations - Fixed a potential crash in `NotificationProxyActivity` - In-app automations and Scenes that were not available during app launch can now be triggered by events that happened in the previous 30 seconds - Added support for additional text styles in Scenes - Added highlight markdown support in Scenes (`==highlighted text==`) - Fixed incrementing frequency limits before a message is ready to display - Improved support for WebViews in Scenes - Added support for Story pause/resume and back/next controls ## 20.0.7 December 30, 2025 Patch release with improvements to notification processing timing that resolves a crash when app is opened from a notification on Android 15. ### Changes - Fixed notification processing timing for Android 15 compatibility ## 20.0.6 December 16, 2025 Patch release to fix a regression in `NotificationIntentProcessor` that interfered with handling of `PendingIntent`s set on custom built notifications. ### Changes - Fixed issue with custom notification handling of `PendingIntent`s in `NotificationIntentProcessor` ## 20.0.5 December 5, 2025 Patch release that fixes an issue with opening the Compose Message Center. ### Changes - Fixed opening of Compose MessageCenterActivity - Merged PushManagerExtensions into PushManager ## 20.0.4 November 25, 2025 Patch release that fixes a potential race condition when setting metadata and creating action arguments concurrently. Apps experiencing crashes when processing push notifications should update to resolve this issue. ### Changes - Fixed potential `ConcurrentModificationException` in `ActionRunRequest` when metadata is modified concurrently with action execution, most likely occurring when processing incoming push notifications ([#258](https://github.com/urbanairship/android-library/issues/258)). ## 20.0.3 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes and minor fixes for the Preference Center Compose module. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. - Allow multiple Preference Centers to be displayed with Preference Center Compose. - Fixed checked/unchecked icon assets for Preference Center Compose. - Updated Preference Center Compose default toolbar to allow `navIcon` to be `null`. ## 19.13.6 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.2 November 4, 2025 Patch release that fixes prompting for permissions on foreground. ### Changes - Fixed prompting for permissions on foreground. - Removed usage of material icons compose library. - Updated Message Center titles to be markes as headings. ## 20.0.1 October 24, 2025 Minor release that fixes packaging and publishing for the modules added in 20.0.0. Apps upgrading to SDK 20.x should update directly to 20.0.1 to ensure proper packaging of these modules. ### Changes - Fixed publishing for: - `urbanairship-message-center-core` - `urbanairship-message-center-compose` - `urbanairship-preference-center-core` - `urbanairship-preference-center-compose` - `urbanairship-debug` ## 20.0.0 October 23, 2025 Major SDK release with several breaking changes. See the [Migration Guide](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-19-20.md) for detailed instructions on upgrading. ### Changes - compileSdkVersion updated to 36 - Kotlin updated to 2.2.0 - The `UAirship` singleton has been deprecated and replaced with `Airship` - `Airship` is no longer a shared instance; instead, it exposes static methods for accessing components - Majority of the SDK has been migrated to Kotlin - Message Center package changes: - `message-center-core`: Core API with no UI - `message-center`: Android XML layouts (depends on `message-center-core`) - `message-center-compose`: New Jetpack Compose UI (depends on `message-center-core`) - Preference Center package changes: - `preference-center-core`: Core API with no UI - `preference-center`: Android XML layouts (depends on `preference-center-core`) - `preference-center-compose`: New Jetpack Compose UI (depends on `preference-center-core`) - New AirshipDebug package that exposes insights and debugging capabilities into the Airship SDK for development builds, providing enhanced visibility into SDK behavior and performance. ## 19.13.5 October 13, 2025 Patch release that handles BigDecimal in our JSON parsing. This prevents parse exceptions if the default Android org.json package is replaced by the org.json maven package. ### Changes - Handle BigDecimal and other number values when parsing JSON from a string. ## 19.13.4 October 6, 2025 Patch release that addresses an issue with handling Play Services errors before `takeOff` and fixes a Scene pager transition bug. ### Changes - Updated `PlayServiceErrorActivity` to handle play services errors before `takeOff` is called. - Fixed Scene pager issue where tapping the scene during a transition from one page to another would interrupt the transition. ## 19.13.3 September 26, 2025 Patch release that fixes an issue with handling `uairship://close` in Message Center and improves Scene accessibility. ### Changes - Fixed handling of `uairship://close` links in Message Center - Improved accessibility for Scene pager indicators - Improved `DeferredResult` logging ## 19.13.2 September 18, 2025 Patch release that adds more logs to the deferred schedules preparing process. ### Changes - Added more logs to the deferred schedules preparing process. ## 19.13.1 September 15, 2025 Patch release to fix an issue with showing out of date In-App Automations and Scenes. ### Changes - Fixed refreshing out of date In-App Automations and Scenes before displaying. ## 19.13.0 September 5, 2025 Minor release that adds support for handling `uairship://message_center/message/<message_id>` links to open a specific message in Message Center. ### Changes - Added support for handling `uairship://message_center/message/<message_id>` links to Message Center ## 19.12.0 September 4, 2025 Minor release that adds a new flag to HTML In-App message content to force full screen on all devices. ### Changes - Added `forceFullScreenDisplay` to HTML In-App message content - Improved accessibility in Scenes by removing labels from being focusable when using keyboard navigation ## 19.11.0 August 21, 2025 Minor release that enforces that incoming pushes are for the current channel ID and adds a manifest metadata entry to control handling of insets for IAM banners for edge-to-edge mode. ### Changes - Added Activity metadata entry (`com.urbanairship.iam.banner.BANNER_INSET_EDGE_TO_EDGE`) to force handling of insets for IAM banners in edge-to-edge mode. - Channel ID is now enforced for incoming pushes, ensuring that only pushes for the current channel ID are processed. ## 18.7.2 August 19, 2025 Patch release that fixes embedded display reporting and a potential crash in Scenes. Apps that use Scenes or Embedded Content should update to this version or later. ### Changes - Fixed an issue that could cause embedded content displays to be reported too early. - Fixed a potential crash that can occur when a Scene is dismissed. ## 19.10.2 August 13, 2025 Patch release that fixes embedded display reporting and a potential crash in Scenes. Apps that use Scenes or Embedded Content should update to this version or later. ### Changes - Fixed an issue that could cause embedded content displays to be reported too early. - Fixed a potential crash that can occur when a Scene is dismissed. ## 19.10.1 August 1, 2025 A patch release that fixes an automation dao crash if an expected nonnull JSON field contains invalid JSON. ### Changes - Fixed potential automation dao crash when an expected nonnull JSON field contains invalid JSON. ## 19.10.0 July 24, 2025 A minor release with accessibility and layout improvements to Scenes, a key performance update, and several bug fixes. ### Changes - Added support in Scenes for linking form inputs to a label for better accessibility. - Added container item alignment to Scenes to change the natural alignment within a container. - Updated the initial remote-data request (IAX, Config, Feature Flags, etc...) to bypass work manager to improve performance. - Fixed setting content-descriptions on a text/number/email input in Scenes to provide better accessibility. - Fixed potential automation dao crash when migrating from an older SDK version. - Fixed an issue where dismissing a Scene with a back gesture could prevent it from displaying again in the same session. ## 19.9.2 July 14, 2025 Patch release with several fixes and accessibility improvements for Scenes. ### Changes - Fixed a crash when dismissing an in-app automation view. - Fixed multiple page views being recorded for pages in branching Scenes. - Fixed a bug in Message Center Message WebView that could potentially interfere with JS in other web views. - Accessibility fixes and improvements for Scenes. ## 19.9.1 June 24, 2025 Patch release that enhances logging, fixes a potential memory leak in paging Scenes, and improves accessibility. ### Changes - Fixed potential memory leak in paging Scenes by improving accessibility listener lifecycle management. - Added 'logPrivacyLevel' to the config to improve managing logging visibility. - Added accessibility dismissal announcement for in-app messages. ## 19.9.0 June 17, 2025 A minor update with enhancements to the Scenes and Message Center functionality and bug fixes for Analytics and Automation. This version is required for Scene branching and phone number collection. ### Changes Analytics: - Fixed bug that could cause locale-based descrepancies in reports. Automation: - Fixed version trigger predicate matching to properly evaluate app version conditions. Message Center: - Automatically retries failed message list refreshes for improved reliability. - Expired messages will no longer trigger a network request to refresh the listing. Scenes: - Fixed layout issues with modal frames, specifically related to margins and borders. - Fixed border rendering issues when stroke thickness exceeds corner radius. - Fixed several issues related to Scene branching. - Added support for custom corner radii on borders. - Added support for more flexible survey toggles. ## 19.8.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Improvements - Improved load times for Scenes by prefetching assets concurrently. ## 19.7.0 May 15, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Fixed minor issue with SMS collection in Scenes where the button loading indicator does not clear if submission encounters an error. ## 19.6.2 April 29, 2025 Patch release that fixes a crash in `PushManager.onTokenChanged`, introduced in release 19.6.0. Apps should skip release 19.6.0 and 19.6.1 and update directly to this version, or later. ### Changes - Fixed nullability of `oldToken` in `PushManager.onTokenChanged`. ## 19.6.1 April 28, 2025 Patch release that fixes a crash with NPS scores within a Scene that uses branching. Apps planning on using the upcoming branching feature should update. ### Changes - Fixed crash with a branching Scene with an NPS widget. ## 19.6.0 April 24, 2025 Minor release adding branching and SMS support for Scenes. ### Changes - Added support for branching in Scenes. - Added support for phone number collection and registration in Scenes. - Added support for setting JSON attributes for Channels and Contacts. - Added a new `mutationsFlow` to `AddTagsAction` and `RemoveTagsAction` to expose tag mutations when applied. - Updated Message Center Inbox to refresh messages on app foreground. ## 19.5.1 April 17, 2025 Patch release with fix for regression in Contacts that could cause a failure to resolve a Contact ID when Contacts are disabled. ### Changes - Fixed Contact ID resolution when contacts are disabled. ## 19.5.0 March 31, 2025 Minor release that adds a public method `Inbox.deleteAllMessages()` and remove restrictions for subclassing `MessageWebView` and `MessageWebViewClient`. ### Changes - Added a new public method `Inbox.deleteAllMessages()` to delete all messages from Message Center. - Removed library group restrictions on `MessageWebView` and `MessageWebViewClient`. ## 19.4.0 March 24, 2025 Minor release that adds support for Custom View in Scenes and fixes Privacy Manager issues when disabling all features. ### Changes - Added Custom View support to enable showing App managed views within a Scene. - Fixed an issue where the Privacy Manager sent multiple opt-out requests after features were disabled following being enabled. ## 19.3.0 March 6, 2025 Minor release that fixes an issue with modal Scenes and adds support for hoisting `AirshipEmbeddedViewGroup` composable state. Apps that make use of Scenes should update to this version or greater. ### Changes - Fix a potential crash when displaying a modal Scene - Added support for hoisting `AirshipEmbeddedViewGroup` composable state ## 18.7.1 February 25, 2025 Patch release to fix a casting exception with Embedded Content. ### Changes - Fixed exception due to a bad cast when using Embedded Content. ## 18.6.1 February 25, 2025 Patch release to fix a casting exception with Embedded Content. ### Changes - Fixed exception due to a bad cast when using Embedded Content. ## 19.2.0 February 21, 2025 Minor release that includes improvements for Scenes and Feature Flags. ### Changes - Added a fade transition for Scenes - Added support for email registration in Scenes - Fixed issues with autoplay videos in Scenes - Improved image download and scaling logic - Fixed an issue with image pre-caching when unable to successfully download an image - Expose rule refresh status for Feature Flags ## 18.7.0 February 7, 2025 Minor release that updates AndroidX libraries. A `compileSdk` of 35+ is required. ### Changes - Updated several AndroidX dependencies - Updated to Kotlin 2.x ## 19.1.0 February 5, 2025 Minor release that fixes an issue with embedded view sizing in scrolling views, improves Message Center accessibility, and replaces usages of `Random` with `SecureRandom`. Apps that make use of Embedded Content or Message Center should update. ### Changes - Fixed an issue with embedded sizing in scrolling views - Improved Message Center Accessibility - Replaced usage of `Random` with `SecureRandom` - Made `MessageWebView` and `MessageWebViewClient` both `public` and `open` for subclassing - Exposed Message Center `ViewModel` state via `LiveData`, in addition to Kotlin `Flow`s - Added `PendingResult` based methods to `Inbox`, for getting read and unread message counts and listing all message IDs ## 19.0.0 January 17, 2025 Major release that adds support for Android 15 (API 35) and updates Message Center and Preference Center to use Material 3. Breaking changes in Message Center are included in this release. See the [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-18-19.md) for more info. ### Changes - The Airship SDK now requires `compileSdk` version 35 (Android 15) or higher, and `minSdk` version 23 (Android 6) or higher. - Migrated Message Center APIs to Kotlin, using asynchronous access patterns. New suspend functions and Flows have been added for Kotlin, and Java APIs have been updated to use `PendingResult` or callbacks. - Rewrote the provided Message Center UI to follow modern Android UI conventions, use Material 3 theming, and support edge-to-edge mode for Android 15. - Updated Preference Center to use Material 3 theming and support edge-to-edge mode for Android 15. - Added `Feature.FEATURE_FLAGS` to `PrivacyManager` to control enablement of feature flags. - Added support for wrapping score views in Scenes. - Added support for Feature Flag experimentation. ## 18.6.0 December 19, 2024 Minor release that updates how Feature Flags are resolved, improves Scene rendering on Android 15, and fixes potential exceptions related to PermissionsManager and PermissionDelegates. ### Changes - Added `resultCache` to `FeatureFlagManager`. This cache is managed by the app and can be optionally used when resolving a flag as a fallback if the flag fails to resolve or if the flag rule set does not exist. - FeatureFlag resolution will now resolve a rule set even if the listing is out of date. - Improved Scene rendering on Android 15, for scenes that do not ignore safe areas. - Prevent potential "Already resumed" exceptions that could be caused by a permission delegate calling the callback multiple times. - Improved constraint version matching ## 18.5.0 December 5, 2024 Minor release that includes various improvements to scenes, data management and some minor bug fixes. ### Changes - Added support to mark a label as a heading in Scenes. - Improved live update database handling to mitigate rare filesystem crashes. - Improved automation store to avoid query limits. ## 18.4.2 November 26, 2024 Patch release that fixes an issue with Embedded Views being impacted by certain App theme customizations, avoids a potential NPE related to network failures, and adds more useful logging around Feature Flag evaluation. ### Changes - Prevent App-level theme customizations from impacting Embedded Views - Avoid a potential NPE related to network failures, when no error body is present - Improved logging around Feature Flag evaluation ## 18.4.1 November 16, 2024 Patch release that fixes an issue with pausing and resuming In-App Automations and avoids a potential crash in the Automation database. ### Changes * Fixed an issue with `AutomationEngine.setEnginePaused(...)` that could prevent message displays when paused an then un-paused * Fixed a potential crash in Automation DB if 1000+ rows are present in the schedules table ## 18.4.0 November 1, 2024 Minor release with several enhancements to Scenes and In-App Automations. ### Changes - Added shadow support for modal Scenes - Added new Scene layout to allow adding actions to anything within a Scene - Added new `AirshipEmbeddedViewGroup` composable to make it possible to show a carousel of embedded views for the same embedded ID - Improved accessibility of scene story indicator. Indicator has been updated to make it obvious which page is active by reducing the height of the inactive pages. Previously this was conveyed only through color - improved accessibility for In-App Automation views - Fixed issue with FCM registration if the FCM application is not configured before Airship starts, causing launch notifications to be ignored ## 18.3.3 October 16, 2024 Patch release that fixes a potential crash when displaying In-App Automation messages, improves WebView security, and improves accessibility in Scenes and Stories. Apps that make use of In-App Automation, Landing Pages, or Message Center should update. ### Changes - Fix a potential crash when displaying In-App messages - Explicitly disallow file and content access in all WebViews - Accessibility improvements for Scenes and Stories ## 18.3.2 October 3, 2024 Patch release that improves markdown support in Scenes and fixes for automation display interval and frequency limit handling. Apps that make use of markdown in Scenes, or automations with display intervals or frequency limits should update. ### Changes - Improve markdown support in Scenes, including better handling of newlines in the input text. - Fixed automation display interval and frequency limit handling. ## 18.3.1 September 30, 2024 Patch release that fixes modal IAA border radius and fixes scenes with wide images. ### Changes - Fixed modal IAA border radius. - Fixed scenes with wide images. ## 18.3.0 September 13, 2024 Minor release that adds a new method `enableUserNotifications(PermissionPromptFallback)` on `PushManager`. ### Changes - Added a `enableUserNotifications(PermissionPromptFallback)` method on `PushManager` that will attempt to enable notifications and use the fallback if the permission is denied. ## 18.2.0 September 6, 2024 Minor release with several enhancements to In-App Automation, Scenes, and Surveys. This version also contains a fix for applications that are targeting API 35. ### Changes - Updated compose bom to 2024.06.00. - Replaced the usage of `removeFirst` to avoid crashes when targeting API 35. - Added ability to customize the content per In-App Automation with the new `InAppMessageContentExtender`. - Added plain markdown support for text markup in Scenes. - Added execution window support to In-App Automation, Scenes, and Surveys. - Updated handling of priority for In-App Automation, Scenes, and Surveys. Priority is now taken into consideration at each step of displaying a message instead of just sorting messages that are triggered at the same time. - Updated handling of long delays for In-App Automation, Scenes, and Surveys. Delays will now be preprocessed up to 30 seconds before it ends before the message is prepared. ## 18.1.6 August 10, 2024 Patch release that fixes in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Fixed Automation Engine updates when pause state changes. ## 18.1.5 August 6, 2024 Patch release that fixes test devices audience check and holdout group experiments displays. ### Changes - Fixed test devices audience check. - Fixed holdout group experiment displays. ## 18.1.4 August 1, 2024 Patch release that includes bug fixes for Embedded Content. ### Changes - Fixed an issue with dismissing Embedded Content after pausing and resuming the app. - Updated the default `PreferenceCenterFragment` to scope the `PreferenceCenterViewModel` to the fragment's view lifecycle. ## 18.1.3 July 30, 2024 Patch release that includes bug fixes for Embedded Content and Preference Center, and accessibility improvements for Message Center. ### Changes - Fixed an issue with container child item measurement in Scenes, when margins were set on the container items. - Fixed a Preference Center bug that could lead to subscription channel chips not being visible when initially displaying a Preference Center. - Fixed dismissing multiple embedded views in the same session. - Fixed an issue with automation trigger state not correctly persisting across sessions. - Message Center accessibility improvements. - Updated the default style for the pull to dismiss view in In-App Message Banners to better match iOS. ## 18.1.2 July 16, 2024 Patch release that includes fixes for Preference Center. ### Changes - Fixed warning message on preference center email entry field. - Fixed country code listing. ## 18.1.1 June 28, 2024 Patch release that includes fixes for Preference Center, Privacy Manager, and Embedded Content. ### Changes - Fixed a Preference Center issue that caused contact subscription toggles to show the incorrect state after being toggled - Fixed test dependency being included in the automation module - Fixed Embedded Content impression event interval - Fixed privacy manager crash when enabling, disabling, or setting an empty set of features - Contact channel listing is now refreshed on foreground and from a background push ## 18.1.0 June 21, 2024 Minor SDK release that fixes a potential crash related to analytics during app init and adds public builders for modifying `InAppMessage` and `AutomationSchedule` objects via extenders set on`LegacyInAppMessaging`. ### Changes - Fixed a potential crash related to analytics during app init - Added builders for modifying `InAppMessage` and `AutomationSchedule` objects via extenders set on `LegacyInAppMessaging` ## 18.0.0 June 14, 2024 Major SDK release with several breaking changes. See the [Migration Guides](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-17-18.md) for more info. ### Changes - The Airship SDK now requires `compileSdk` version 34 (Android 14) or higher. - New Automation module - Check schedule’s start date before executing, to better handle updates to the scheduled start date - Improved image loading for In-App messages, Scenes, and Surveys - Reset GIF animations on visibility change in Scenes and Surveys - Pause Story progress while videos are loading - Concurrent automation processing to reduce latency if more than one automation is triggered at the same time - Embedded Scenes & Survey support - New module `urbanairship-automation-compose` to support embedding a Scene & Survey in compose - Added new compound triggers and IAX event triggers - Ban lists support - Added new `PrivacyManager.Feature.FEATURE_FLAGS` to control access to feature flags - Added support for multiple deferred feature flag resolution - Added contact management support in preference centers - Migrated to non-transitive R classes - Removed `urbanairship-ads-identifier` and `urbanairship-preference` modules Initial alpha release of SDK 18.0.0. This version is not suitable for a production app, but we encourage testing out the new APIs and providing us feedback so we can make changes before the final SDK 18 release. The Airship SDK now requires `compileSdk` version 34 (Android 14) or higher. ### Changes - Improved image loading for In-App messages, Scenes, and Surveys - Reset GIF animations on visibility change in Scenes and Surveys - Pause Story progress while videos are loading - Migrated to non-transitive R classes - Check schedule’s start date before executing, to better handle updates to the scheduled start date - Removed `urbanairship-ads-identifier` and `urbanairship-preference` modules See the [Migration Guide](https://github.com/urbanairship/android-library/tree/main/documentation/migration/migration-guide-17-18.md) for further details. [View Older Releases](https://github.com/urbanairship/android-library/releases?q=created%3A%3C2024-05-15&expanded=true) # Android SDK Resources > API references and other resources for Android development. ## Platform Support {#platform-support} | Feature | Android | Android TV | Fire OS | |----------------------------------------|---------|------------|---------| | Push Notifications | ✅ | ❌ | ✅ | | Live Updates | ✅ | ❌ | ❌ | | In-App Experiences | ✅ | ✅ ¹ | ✅ ² | | Embedded Content | ✅ | ✅ | ✅ | | Message Center | ✅ | ✅ ³ | ✅ ⁴ | | Preference Center | ✅ | ✅ | ✅ | | Feature Flags | ✅ | ✅ | ✅ | | Analytics | ✅ | ✅ | ✅ | | Contacts | ✅ | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ✅ | ¹ **Android TV In-App Experiences:** Modal, Fullscreen, and Banner message styles are supported. Standard In-App Messages are not supported. ² **Fire OS In-App Experiences:** Standard In-App Messages are supported. In-App Automation is not supported. ³ **Android TV Message Center:** The OpenExternalUrlAction to open URLs in messages will not work, as Android TV does not have a web browser. ⁴ **Fire OS Message Center:** The MessageCenterAction opens the Message Center but does not directly open the message. If a web browser is installed, URLs function as button actions. ## API references - [Android API Docs](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/) ## Github Samples * [Sample Apps](https://github.com/urbanairship/android-sample-apps) ## Source * [Source](https://github.com/urbanairship/android-library) ## Changelog * [Changelog](https://www.airship.com/docs/developer/sdk-integration/android/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Android license](https://github.com/urbanairship/android-library/blob/main/LICENSE) ## SDK Installation Complete installation and configuration guides for the Airship SDK, including setup, advanced integration, logging, and locale configuration. # Install and Set Up the Android SDK > Learn how to install the Airship SDK on Android, Android TV, and Fire OS. The Airship Android SDK is a modular, Kotlin-first SDK with support for both Jetpack Compose and XML Views. It provides a consistent API for push notifications, in-app messaging, and audience management. For a complete reference of feature support across Android, Android TV, and Fire OS, see [Platform Support](https://www.airship.com/docs/developer/sdk-integration/android/resources/#platform-support). > **Tip:** If you use an AI coding assistant, you can connect it to Airship with Skills and an MCP server. See [Airship AI Tools](https://www.airship.com/docs/developer/ai-tools/ai-tools/). ## Requirements * Minimum Android version supported: `23+` * Compile SDK version: `36+` ## SDK Installation The Airship SDK is split into modules which allow you to choose the push providers and features to be included in your application. | Module | Description | |------------------------------------------|----------------------------------------------------------------------------------| | `urbanairship-adm` | ADM push provider | | `urbanairship-fcm` | FCM push provider | | `urbanairship-hms` | HMS push provider | | `urbanairship-automation` | In-App Automation, In-App Messaging, and Landing pages (XML Views UI) | | `urbanairship-automation-compose` | In-App Automation, In-App Messaging, and Landing pages (Compose UI) | | `urbanairship-message-center` | Message Center (XML Views UI) | | `urbanairship-message-center-compose` | Message Center (Compose UI) | | `urbanairship-preference-center` | Preference Center (XML Views UI) | | `urbanairship-preference-center-compose` | Preference Center (Compose UI) | | `urbanairship-live-update` | Live Updates | | `urbanairship-feature-flag` | Feature Flags | Choose the UI framework that matches your app: use `-compose` modules if using Jetpack Compose, otherwise use the standard modules. Do not include both modules in the same app. #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" // FCM push provider implementation("com.urbanairship.android:urbanairship-fcm:$airshipVersion") // In-App Messaging implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion") // Message Center implementation("com.urbanairship.android:urbanairship-message-center-compose:$airshipVersion") } ``` > **Note:** All Airship dependencies included in the `build.gradle.kts` file should specify the exact same version. #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" // FCM push provider implementation "com.urbanairship.android:urbanairship-fcm:$airshipVersion" // In-App Messaging implementation "com.urbanairship.android:urbanairship-automation:$airshipVersion" // Message Center implementation "com.urbanairship.android:urbanairship-message-center:$airshipVersion" } ``` > **Note:** All Airship dependencies included in the `build.gradle` file should specify the exact same version. ## Initialize Airship The Airship SDK must be initialized before any Receiver, Service, or Activity is created. The recommended way to achieve this is by using the [Autopilot](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-autopilot/index.html) class, but it is also possible to call *takeOff* manually in **Application.onCreate**. ### Setup Autopilot The Airship SDK will automatically launch and load an Autopilot class that can be used to provide custom Airship config and to customize the Airship instance. Autopilot is the recommended approach to integrating the Airship SDK. To start, create a new class that extends `Autopilot`. This class needs to be public and have a default no argument constructor. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { } ``` Add metadata within the `application` entry to the `AndroidManifest.xml`. The name of the meta-data `com.urbanairship.autopilot` and the value should be the fully qualified class name. **Register the extended Autopilot class** ```xml ``` ### Configuring Airship Airship requires your project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret)to authenticate your application. To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. The [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/index.html) provides the app credentials and common settings for Airship. To provide an `AirshipConfigOptions` instance, override the method `createAirshipConfigOptions` in your autopilot class. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { override fun createAirshipConfigOptions(context: Context) = airshipConfigOptions { // Set default credentials. Alternatively, you can set production and development separately. setAppKey("YOUR_DEFAULT_APP_KEY") setAppSecret("YOUR_DEFAULT_APP_SECRET") // Set site. (Either Site.SITE_US or Site.SITE_EU) setSite(Site.SITE_US) // Notification config setNotificationAccentColor(context.getColor(R.color.accent)) setNotificationIcon(R.drawable.ic_notification) setNotificationChannel(NotificationProvider.DEFAULT_NOTIFICATION_CHANNEL) } override fun onAirshipReady(context: Context) { // Airship is ready! Configure additional settings here. Airship.push.userNotificationsEnabled = true } } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { @Override public @Nullable AirshipConfigOptions createAirshipConfigOptions(@NotNull Context context) { return AirshipConfigOptions.newBuilder() // Set default credentials. Alternatively, you can set production and development separately. .setAppKey("YOUR_DEFAULT_APP_KEY") .setAppSecret("YOUR_DEFAULT_APP_SECRET") // Set site. (Either SITE_US or SITE_EU) .setSite(Site.SITE_US) // Notification config .setNotificationAccentColor(context.getColor(R.color.accent)) .setNotificationIcon(R.drawable.ic_notification) .setNotificationChannel(NotificationProvider.DEFAULT_NOTIFICATION_CHANNEL) .build(); } @Override public void onAirshipReady(@NotNull Context context) { // Airship is ready! Configure additional settings here. Airship.getPush().setUserNotificationsEnabled(true); } } ``` > **Note:** Airship config defaults to the US cloud site (`Site.SITE_US`). If your application is set up for the EU site, you must set the site on the config options to `Site.SITE_EU`. ### Customizing Airship behavior {#customizing-airship} Airship provides common config options in `AirshipConfig`, however some more advanced configuration must be set directly on the Airship instance. Custom push handling, deep linking, etc., should be configured during `onAirshipReady` callback. This will ensure Airship is properly configured before handling any messages. The `onAirshipReady` callback will be called on a background thread during Airship init process. Applications should not do any long-running work during this callback, or it might prevent Airship from being ready in time to process a push notification. #### Android Kotlin ```kotlin class SampleAutopilot : Autopilot() { // ... override fun onAirshipReady(context: Context) { // Custom Message Center MessageCenter.shared().setOnShowMessageCenterListener { messageId: String? -> true } // Notification handling Airship.push.addPushListener(MyPushListener()) Airship.push.notificationListener = MyNotificationListener() // etc... } } ``` #### Android Java ```java public class SampleAutopilot extends Autopilot { // ... @Override public void onAirshipReady(@NonNull Context context) { MessageCenter.shared().setOnShowMessageCenterListener(messageId -> { return true; }); Airship.getPush().addPushListener(new MyPushListener()); Airship.getPush().setNotificationListener(new MyNotificationListener()); } } ``` ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** on your Android device or emulator. 2. **Check the logcat output** for Airship channel creation: - Look for a log message similar to: `Airship channel created: ` - The channel ID will appear in the log output, confirming successful initialization. - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/android/installation/logging/). If you see the channel ID in the logcat and no errors, your integration is successful. You can now proceed with configuring [deep links](https://www.airship.com/docs/developer/sdk-integration/android/deep-links/), [push notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/getting-started/), and other Airship features. If you don't see a channel ID in the logcat or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/android/troubleshooting/initialization/) for common problems and solutions. # Advanced Integration > Additional configuration for specific use-cases. ## Configuring Airship via properties file If no config is returned from `Autopilot.createAirshipConfigOptions`, Airship will default to loading config from the `airshipconfig.properties` file, located in your application's `assets` directory. This can be useful for in Apps with multiple build variants, or for keeping credentials out of version control. Sample `assets/airshipconfig.properties` file: ```properties # App credentials app_key = YOUR_DEFAULT_APP_KEY app_secret = YOUR_DEFAULT_APP_SECRET # Optionally, set separate production credentials # production_app_key = YOUR_PRODUCTION_APP_KEY # production_app_secret = YOUR_PRODUCTION_APP_SECRET # development_app_key = YOUR_DEVELOPMENT_APP_KEY # development_app_secret = YOUR_DEVELOPMENT_APP_SECRET # Set the cloud site (either SITE_US or SITE_EU) site = SITE_US # In production (true/false) in_production = false # Enable Airship debug logging (true/false) is_airship_debug_enabled = true # Notification configuration notification_icon = @drawable/ic_notification notification_accent_color = @color/accent notification_channel = default_channel ``` The keys used in the `airshipconfig.properties` file match the field names in [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/index.html) , converted to snake-case. ## URL allowlist The [UrlAllowList](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-url-allow-list/index.html) controls which URLs the Airship SDK is able to act on. The SDK divides up usages of URLs into two different scopes: - `SCOPE_OPEN_URL`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, or displayed in an HTML in-app message. Defaults to allowing all URLs if not specified in the config. - `SCOPE_JAVASCRIPT_INTERFACE`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship originated URLs. Allowed URLs should be provided when configuring the Airship Config options. **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` ## Custom Firebase applications By default, the Airship SDK will use the data in `google-services.json` to configure Firebase Messaging. If your app makes use of multiple Firebase projects, you can instruct the Airship SDK to use a specific named Firebase project for Firebase Cloud Messaging (FCM). In order to create a secondary Firebase application instance, you'll need to manually configure `FirebaseOptions` and initialize your secondary Firebase application. The Firebase application should be initialized before `takeOff`, or during the `onAirshipReady` callback. #### Android Kotlin ```kotlin // Manually configure FirebaseOptions for the secondary Firebase Application. val options = FirebaseOptions.Builder() .setProjectId("Your Firebase Project ID") .setApplicationId("Your Firebase Application ID") .setApiKey("Your Firebase API key") .setGcmSenderId("Your GCM Sender ID") .build() // Initialize the secondary Firebase Application. Firebase.initialize(context, options, "secondary") ``` #### Android Java ```java // Manually configure FirebaseOptions for the secondary Firebase Application. FirebaseOptions options = new FirebaseOptions.Builder() .setProjectId("Your Firebase Project ID") .setApplicationId("Your Firebase Application ID") .setApiKey("Your Firebase API key") .setGcmSenderId("Your GCM Sender ID") .build(); // Initialize the secondary Firebase Application. FirebaseApp.initializeApp(context, options, "secondary"); ``` Now that you have initialized your secondary Firebase application, you can configure the Airship SDK to use it by setting [Firebase app name](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/-builder/set-fcm-firebase-app-name.html) name in the `AirshipConfigOptions` instance. ## Extending the FirebaseMessagingService If your application uses its own `FirebaseMessagingService` or some other third party push provider, you will also need to forward `onNewToken` and `onMessageReceived` calls to the Airship SDK. #### Android Kotlin ```kotlin fun onNewToken(token: String) { AirshipFirebaseIntegration.processNewToken(getApplicationContext(), token) } fun onMessageReceived(remoteMessage: RemoteMessage) { AirshipFirebaseIntegration.processMessageSync(getApplicationContext(), message) } ``` #### Android Java ```java @Override public void onNewToken(String token) { AirshipFirebaseIntegration.processNewToken(getApplicationContext(), token); } @Override public void onMessageReceived(RemoteMessage message) { AirshipFirebaseIntegration.processMessageSync(getApplicationContext(), message); } ``` ## Extending the HmsMessageService If your application uses its own `HmsMessageService` or some other third party push provider, you will also need to forward `onNewToken` and `onMessageReceived` calls to the Airship SDK. #### Android Kotlin ```kotlin fun onNewToken(token: String?) { AirshipHmsIntegration.processNewToken(getApplicationContext(), token) } fun onMessageReceived(remoteMessage: RemoteMessage) { AirshipHmsIntegration.processMessageSync(getApplicationContext(), message) } ``` #### Android Java ```java @Override public void onNewToken(@Nullable String token) { AirshipHmsIntegration.processNewToken(getApplicationContext(), token); } @Override public void onMessageReceived(RemoteMessage message) { AirshipHmsIntegration.processMessageSync(getApplicationContext(), message); } ``` # Logging > Configure log levels, privacy settings, and custom log handlers to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. If you don't configure logging, the SDK uses **Info** for development builds and **Error** for production builds with **private** privacy level. ## Log levels The log level acts as a minimum threshold—only logs at that level and higher will be logged. Available log levels, ordered from most to least verbose: | Log Level | Prefix | Description | | :-------- | :----- | :---------- | | **Verbose** | `V` | Highly detailed SDK status for deep debugging and troubleshooting | | **Debug** | `D` | General SDK status with more detailed information than Info | | **Info** | `I` | General SDK status and lifecycle events | | **Warning** | `W` | API deprecations, invalid setup, and other recoverable issues | | **Error** | `E` | Critical errors and exceptions that the SDK cannot gracefully handle | | **Assert** | — | Disables all logging | ## Log privacy levels Control the visibility of log contents using privacy levels. This is especially useful when debugging release builds without exposing sensitive information. - **private** (default): Uses the standard `android.util.Log`. In production builds, `verbose` and `debug` messages are completely dropped and will not be logged. Use this for production builds to protect sensitive data. - **public**: Sends all logs with a `public` privacy level, making it easier to capture detailed information from release builds. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. > **Note:** When using `public` privacy level, `verbose` and `debug` log messages are automatically elevated to `info` level to ensure all detailed logs are visible when debugging production builds. ## Configuration You can set separate log levels and privacy levels for development and production builds in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#calling-takeoff). ### Common configuration Typical setup: more verbose logging for development, minimal logging for production: #### Kotlin ```kotlin airshipConfigOptions { // Development: verbose logging for debugging setDevelopmentLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) setDevelopmentLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // Production: minimal logging to reduce noise setProductionLogLevel(AirshipConfigOptions.LogLevel.ERROR) setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PRIVATE) // ... } ``` #### Java ```java AirshipConfigOptions.newBuilder() // Development: verbose logging for debugging .setDevelopmentLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) .setDevelopmentLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // Production: minimal logging to reduce noise .setProductionLogLevel(AirshipConfigOptions.LogLevel.ERROR) .setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PRIVATE) ... ``` ### Debugging production issues When debugging issues in production builds, temporarily enable verbose logging to capture detailed SDK behavior: #### Kotlin ```kotlin airshipConfigOptions { // Production debugging: enable verbose logs setProductionLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) // ... } ``` #### Java ```java AirshipConfigOptions.newBuilder() // Production debugging: enable verbose logs .setProductionLogLevel(AirshipConfigOptions.LogLevel.VERBOSE) .setProductionLogPrivacyLevel(AirshipConfigOptions.PrivacyLevel.PUBLIC) ... ``` ## Verifying log level You can confirm the current log level by checking the Airship log output in your console. On Android, Airship logs use the standard log priority tags (e.g., `V`, `D`, `I`). To find them, filter your logs for the tag **`UAirship`** and observe the priority letter. **Example (Verbose Log)** The `V` in the output indicates a `VERBOSE` level log. ```text Sample - UALib com.urbanairship.sample V UAirship - !SDK-VERSION-STRING!:com.urbanairship.android:urbanairship-core:16.9.0 ``` ## Custom log handler You can provide a custom log handler to intercept and handle all Airship log messages. This is useful when you need to integrate Airship logs with your own logging system or customize how logs are formatted or stored. When a custom log handler is set, the default Airship log handler is completely replaced. Log level filtering is performed before your handler is called, so your handler will only receive logs that meet the configured log level threshold. Implement the `AirshipLogHandler` interface and set it on `UALog.logHandler` before calling `Airship.takeOff()`: #### Kotlin ```kotlin // Example log handler to forward logs to Android logcat and upload to a remote logging service val customLogHandler = AirshipLogHandler { tag, logLevel, throwable, message -> val msg = message() // Forward to Android logcat when (logLevel) { Log.VERBOSE -> Log.v(tag, msg, throwable) Log.DEBUG -> Log.d(tag, msg, throwable) Log.INFO -> Log.i(tag, msg, throwable) Log.WARN -> Log.w(tag, msg, throwable) Log.ERROR -> Log.e(tag, msg, throwable) else -> Unit // Do nothing } // Optionally: send to remote logging service MyLoggingService.log(tag, logLevel, msg, throwable) } // Set the custom handler globally, before Airship.takeOff() UALog.logHandler = customLogHandler ``` #### Java ```java AirshipLogHandler logHandler = new AirshipLogHandler() { @Override public void log(@NotNull String tag, int logLevel, @Nullable Throwable throwable, @NotNull Function0<@NotNull String> message) { String msg = message.invoke(); // Forward to Android logcat switch (logLevel) { case Log.VERBOSE: Log.v(tag, msg, throwable); break; case Log.DEBUG: Log.d(tag, msg, throwable); break; case Log.INFO: Log.i(tag, msg, throwable); break; case Log.WARN: Log.w(tag, msg, throwable); break; case Log.ERROR: Log.e(tag, msg, throwable); break; default: break; // Do nothing } // Optionally: send to remote logging service MyLoggingService.log(tag, logLevel, msg, throwable); } }; // Set the custom handler globally, before Airship.takeOff() UALog.setLogHandler(logHandler); ``` # Locale > Configure locale behavior and override the default locale that Airship uses. Airship uses the [Locale](https://www.airship.com/docs/reference/glossary/#locale) for various SDK operations. By default, the SDK automatically uses the device's locale settings, but you can configure it to use the user's preferred language or override it programmatically. ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. #### Kotlin ```kotlin Airship.localeManager.setLocaleOverride(Locale.GERMANY) ``` #### Java ```java Airship.getLocaleManager().setLocaleOverride(new Locale("de")); ``` ## Clearing the locale override To remove a locale override and return to using the device's locale settings: #### Kotlin ```kotlin Airship.localeManager.setLocaleOverride(null) ``` #### Java ```java Airship.getLocaleManager().setLocaleOverride(null); ``` ## Getting the current locale To retrieve the locale that Airship is currently using: #### Kotlin ```kotlin val airshipLocale = Airship.localeManager.locale ``` #### Java ```java Locale airshipLocale = Airship.getLocaleManager().getLocale(); ``` ## Push Notifications Comprehensive guides for implementing push notifications, including setup, notification channels, and advanced customizations. # Push Notifications > How to configure your application to receive and respond to notifications. ## Push Provider Setup Configure a push provider to enable push notifications on Android devices. ### FCM 1. Follow [FCM Android Setup](https://firebase.google.com/docs/android/setup) to configure your Android application to connect to Firebase. 1. Add the `urbanairship-fcm` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-fcm:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-fcm:$airshipVersion" ``` ### ADM 1. Follow [Amazon's documentation](https://developer.amazon.com/docs/adm/integrate-your-app.html#store-your-api-key-as-an-asset) to store your API key as an asset. 1. Add the `urbanairship-adm` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-adm:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-adm:$airshipVersion" ``` ### HMS 1. Follow [Huawei's documentation](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084) to set up the HMS SDK. > **Note:** Airship requires HMS Core Push SDK 6.3.0.304 or newer. 1. Add the `urbanairship-hms` dependency to your application's build.gradle file: #### Gradle Kotlin ```kotlin implementation("com.urbanairship.android:urbanairship-hms:$airshipVersion") ``` #### Gradle Groovy ```groovy implementation "com.urbanairship.android:urbanairship-hms:$airshipVersion" ``` ## Enable User Notifications Enabling `userNotificationsEnabled` will prompt the user for permission to send notifications. To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately, and instead wait for a more appropriate time in the app. The Airship SDK makes a distinction between `user notifications`, which can be seen by the user, and other forms of push that allow you to send data to your app silently, or in the background. Enabling or disabling user notifications is a preference often best left up to the user, so by default, user notifications are disabled. #### Kotlin ```kotlin Airship.push.userNotificationsEnabled = true ``` #### Java ```java Airship.getPush().setUserNotificationsEnabled(true); ``` > **Note:** For apps that target Android 13 (API 33) and above, enabling user notifications will display a runtime permission prompt to allow notifications to be sent. > > To increase the likelihood that the user will accept, you should avoid prompting the user for permission immediately on app startup, and instead wait for a more appropriate time to prompt for notification permission. ## Handle Notification Events The Airship SDK provides several callbacks for when a push is received or a notification is interacted with. Apps can use these callbacks to do custom push processing. Registering for a callback is optional, the SDK will automatically launch the application without the need to set a callback. #### Kotlin ```kotlin Airship.push.addPushListener { message: PushMessage, notificationPosted: Boolean -> // Called when a message is received } Airship.push.notificationListener = object : NotificationListener { override fun onNotificationPosted(notificationInfo: NotificationInfo) { // Called when a notification is posted } override fun onNotificationOpened(notificationInfo: NotificationInfo): Boolean { // Called when a notification is tapped. // Return false here to allow Airship to auto launch the launcher activity return false } override fun onNotificationForegroundAction( notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo ): Boolean { // Called when a notification action button is tapped. // Return false here to allow Airship to auto launch the launcher activity return false } override fun onNotificationBackgroundAction( notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo ) { // Called when a background notification action button is tapped. } override fun onNotificationDismissed(notificationInfo: NotificationInfo) { // Called when a notification is dismissed } } ``` #### Java ```java Airship.getPush().addPushListener((message, notificationPosted) -> { // Called when any push is received }); Airship.getPush().setNotificationListener(new NotificationListener() { @Override public void onNotificationPosted(@NonNull NotificationInfo notificationInfo) { // Called when a notification is posted } @Override public boolean onNotificationOpened(@NonNull NotificationInfo notificationInfo) { // Called when a notification is tapped. // Return false here to allow Airship to auto launch the launcher activity return false; } @Override public boolean onNotificationForegroundAction(@NonNull NotificationInfo notificationInfo, @NonNull NotificationActionButtonInfo actionButtonInfo) { // Called when a notification action button is tapped. // Return false here to allow Airship to auto launch the launcher activity return false; } @Override public void onNotificationBackgroundAction(@NonNull NotificationInfo notificationInfo, @NonNull NotificationActionButtonInfo actionButtonInfo) { // Called when a background notification action button is tapped. } @Override public void onNotificationDismissed(@NonNull NotificationInfo notificationInfo) { // Called when a notification is dismissed } }); ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. For Android, all push messages are delivered in the background, but default Airship will treat messages without an `alert` as silent. > **Note:** Pushes sent without an `alert` property do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by Android and FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## Next Steps - [Notification Channels](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#notification-channels) - Create custom notification channels for Android - [Custom Notification Provider](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#custom-notification-provider) - Customize how notifications are displayed - [Interactive Notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/advanced-customizations/#interactive-notifications) - Add action buttons to notifications If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/android/troubleshooting/push-notifications/) to check notification status and fix common issues. # Advanced Customizations > Customize notification appearance, behavior, and interactions with Android-specific features. ## Notification channels Starting with Android O, each notification must assign a valid notification channel or the notification will not display. The SDK will automatically assign a default channel with the name "Notifications". The default channel can be overridden either in [AirshipConfigOptions](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship-config-options/-builder/set-notification-channel.html) or the notification channel can also be set per message by specifying the channel's ID in the [Push API](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). ### Compat notification channels Notification channel compat allows you to define notification channels for all Android versions. For pre-O Android devices, the Airship SDK will apply the notification channel settings on the notification before posting. This allows an app to define channels and use them across all devices. #### Kotlin ```kotlin override fun onAirshipReady(context: Context) { // ... val channelCompat = NotificationChannelCompat( "customChannel", "Breaking News!", NotificationManagerCompat.IMPORTANCE_DEFAULT ) Airship.push .notificationChannelRegistry .createNotificationChannel(channelCompat) } ``` #### Java ```java @Override public void onAirshipReady(@NonNull Context context) { // ... NotificationChannelCompat channelCompat = new NotificationChannelCompat( "customChannel", "Breaking News!", NotificationManagerCompat.IMPORTANCE_DEFAULT); Airship.getPush() .getNotificationChannelRegistry() .createNotificationChannel(channelCompat); } ``` > **Note:** The Airship SDK will fall back to the default notification channel if you set a notification channel ID that doesn't exist, so make sure that you created one with the same ID. ## Clearing notifications Notifications can be cleared manually by using standard Android APIs on the [NotificationManager](https://developer.android.com/reference/android/app/NotificationManager.html) or [NotificationManagerCompat](https://developer.android.com/reference/android/support/v4/app/NotificationManagerCompat.html) classes. #### Kotlin ```kotlin NotificationManagerCompat.from(context).cancelAll() ``` #### Java ```java NotificationManagerCompat.from(context).cancelAll(); ``` ## Control foreground notification display By default, notifications are displayed even when the app is in the foreground. To override that per message, set `foregroundNotificationDisplayPredicate` on `Airship.push`. The predicate runs for every incoming push; return `true` to present the notification or `false` to suppress it. Set the predicate to `null` to restore the default behavior. #### Kotlin ```kotlin Airship.push.foregroundNotificationDisplayPredicate = Predicate { message -> // Example: only show when getExtra("display_in_foreground") == "true" message.getExtra("display_in_foreground") == "true" } ``` #### Java ```java Airship.getPush().setForegroundNotificationDisplayPredicate(message -> { // Example: only show when getExtra("display_in_foreground") == "true" return "true".equals(message.getExtra("display_in_foreground")); }); ``` ## Custom notification provider The `AirshipNotificationProvider` is the recommended factory as it provides full support for all of the [Android push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). All incoming push notifications are built using a class that implements the [NotificationProvider](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-notification-provider/index.html) . The Airship SDK uses the [AirshipNotificationProvider](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-airship-notification-provider/index.html) . With this provider, the standard Android Notification layout will use the application's title and icon. A default big text style will be applied for all notifications. ![Default notification layout using AirshipNotificationProvider](https://www.airship.com/docs/images/default-factory-notification_hu_828cc11f66780a38.webp) *Default notification layout using AirshipNotificationProvider* ## Custom notification factory Custom notification factories are supported, but may cause some [Android Push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject) to no longer work. Only features that the custom factory implements will be available. #### Kotlin ```kotlin class CustomNotificationFactory : NotificationProvider { @WorkerThread override fun onCreateNotificationArguments(context: Context, message: PushMessage): NotificationArguments { val channel = requireNotNull(message.getNotificationChannel("defaultChannel")) return NotificationArguments.newBuilder(message) .setNotificationChannelId(channel) .setNotificationId(message.notificationTag, NotificationIdGenerator.nextID()) .build() } @WorkerThread override fun onCreateNotification(context: Context, arguments: NotificationArguments): NotificationResult { val message = arguments.message // do not display a notification if there is not an alert if (message.alert.isNullOrEmpty()) { return NotificationResult.cancel() } // Build the notification val builder = NotificationCompat.Builder(context) .setContentTitle("Notification title") .setContentText(message.alert) .setAutoCancel(true) .setSmallIcon(R.drawable.notification_icon) // Notification action buttons builder.extend(ActionsNotificationExtender(context, message, notificationId)) return NotificationResult.notification(builder.build()) } @WorkerThread override fun onNotificationCreated(context: Context, notification: Notification, arguments: NotificationArguments) { // Called after the notification is built, right before posting the notifications. Apply any global // defaults to the notification } } ``` #### Java ```java public class CustomNotificationFactory implements NotificationProvider { @WorkerThread @NonNull @Override public NotificationArguments onCreateNotificationArguments(@NonNull Context context, @NonNull PushMessage message) { String channel = message.getNotificationChannel("defaultChannel"); return NotificationArguments.newBuilder(message) .setNotificationChannelId(channel) .setNotificationId(message.getNotificationTag(), NotificationIdGenerator.nextID()) .build(); } @WorkerThread @NonNull @Override public NotificationResult onCreateNotification(@NonNull Context context, @NonNull NotificationArguments arguments) { PushMessage message = arguments.getMessage(); // do not display a notification if there is not an alert if (UAStringUtil.isEmpty(message.getAlert())) { return NotificationResult.cancel(); } // Build the notification NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setContentTitle("Notification title") .setContentText(message.getAlert()) .setAutoCancel(true) // Make sure that your icon follows Google's Guidelines : a white icon with transparent background .setSmallIcon(R.drawable.notification_icon); // Notification action buttons builder.extend(new ActionsNotificationExtender(context, message, notificationId)); return NotificationResult.notification(builder.build); } @WorkerThread public void onNotificationCreated(@NonNull Context context, @NonNull Notification notification, @NonNull NotificationArguments arguments) { // Called after the notification is built, right before posting the notifications. Apply any global // defaults to the notification } } ``` For simple modifications, it is recommended to extend the `AirshipNotificationProvider` instead to ensure all Airship push features continue to work. #### Kotlin ```kotlin class CustomNotificationFactory(context: Context, configOptions: AirshipConfigOptions) : AirshipNotificationProvider(context, configOptions) { @WorkerThread override fun onExtendBuilder( context: Context, builder: NotificationCompat.Builder, arguments: NotificationArguments ): NotificationCompat.Builder { val newBuilder = super.onExtendBuilder(context, builder, arguments) // Apply any defaults to the builder return newBuilder } } ``` #### Java ```java public class CustomNotificationFactory extends AirshipNotificationProvider { public CustomNotificationFactory( @NonNull Context context, @NonNull AirshipConfigOptions configOptions ) { super(context, configOptions); } @WorkerThread @NonNull @Override protected NotificationCompat.Builder onExtendBuilder( @NonNull Context context, @NonNull NotificationCompat.Builder builder, @NonNull NotificationArguments arguments ) { builder = super.onExtendBuilder(context, builder, arguments); // Apply any defaults to the builder return builder; } } ``` #### Kotlin ```kotlin override fun onAirshipReady(context: Context) { Airship.push.notificationProvider = CustomDefaultNotificationProvider() } ``` #### Java ```java @Override public void onAirshipReady(Context context) { Airship.getPush() .setNotificationProvider(new CustomDefaultNotificationProvider()); } ``` ## Available notification extenders The SDK also provides several notification builder extenders to help support [Android Push features](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#androidoverrideobject). [ActionsNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-actions-notification-extender/index.html) : Supports Android Notification Action Button API features. [PublicNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-public-notification-extender/index.html) : Supports Public Notification API features. [StyleNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-style-notification-extender/index.html) : Supports Android style API features. [WearableNotificationExtender](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-wearable-notification-extender/index.html) : Supports Android Wear API features. ## Interactive notifications You can add action buttons to notifications to allow users to interact without opening the app. ### Standard interactive notifications Airship provides a set of standard Interactive Notification types (See: [Built-In Interactive Notification Types](https://www.airship.com/docs/reference/messages/built-in-interactive-notifications/)). It is the *type* that determines which buttons and corresponding labels will be available when you send a push. See the next section for where to specify that in the push payload. You control what happens when you send the push separately, by tying each button ID to a specific action. ### Custom interactive notification types (notification action button groups) If you want to define a custom Interactive Notification type, you must register a new notification action button group. > **Note:** Airship reserves category IDs prefixed with `ua_`. Any custom groups with that prefix will be dropped. Custom [NotificationActionButtonGroups](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push.notifications/-notification-action-button-group/index.html) are supported by registering the groups with the [PushManager](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push/-push-manager/index.html) right after [UAirship.takeOff](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship/-airship/take-off.html) using the [PushManager.addNotificationActionButtonGroup](https://www.airship.com/docs/reference/libraries/android-kotlin/latest/urbanairship-core/com.urbanairship.push/-push-manager/add-notification-action-button-group.html) method. #### Kotlin ```kotlin // Define actions for the group val hiButtonAction: NotificationActionButton = NotificationActionButton.Builder("hi") .setLabel(R.string.hi) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build() val byeButtonAction: NotificationActionButton = NotificationActionButton.Builder("bye") .setLabel(R.string.bye) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build() // Define the group val buttonGroup = NotificationActionButtonGroup.Builder() .addNotificationActionButton(hiButtonAction) .addNotificationActionButton(byeButtonAction) .build() // Add the custom group Airship.push.pushManager.addNotificationActionButtonGroup("custom group", buttonGroup) ``` #### Java ```java // Define actions for the group NotificationActionButton hiButtonAction = new NotificationActionButton.Builder("hi") .setLabel(R.string.hi) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build(); NotificationActionButton byeButtonAction = new NotificationActionButton.Builder("bye") .setLabel(R.string.bye) .setIcon(R.drawable.your_icon_file) .setPerformsInForeground(true) .build(); // Define the group NotificationActionButtonGroup buttonGroup = new NotificationActionButtonGroup.Builder() .addNotificationActionButton(hiButtonAction) .addNotificationActionButton(byeButtonAction) .build(); // Add the custom group Airship.getPushManager().addNotificationActionButtonGroup("custom group", buttonGroup); ``` ## In-App Experiences Implement Scenes, In-App Automations, custom views, and embedded content to deliver engaging in-app experiences to your users. # In-App Experiences > Integrate Scenes & In-App Automations into your Android app to display embedded content and create custom in-app experiences with minimal code. In-App Experiences use Airship's on-device automation framework to provide instant, personalized content that integrates natively with your app. This includes [Scenes](https://www.airship.com/docs/reference/glossary/#scene), which can be displayed as modal or fullscreen overlays or embedded directly within your app screens, and In-App Automations (IAA), which power banner, modal, and fullscreen in-app messages triggered by events. Scenes are fully customizable in the Airship dashboard and require minimal SDK integration. For advanced In-App Automation customization options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/in-app-automation/). ## Requirements To use In-App Experiences, you need: - The `urbanairship-automation` module installed (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/)) - Airship SDK initialized (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/)) > **Note:** **In-App Experiences work out of the box**: Once you install the `urbanairship-automation` module and initialize Airship, In-App Experiences will function automatically. The rest of this documentation covers optional customization and advanced features. ## Controlling display Control when and how In-App Experiences are displayed in your app. You can auto-pause displays on launch (useful for splash screens), manually pause all in-app displays, set intervals between displays, and control when individual messages are ready to display. ### Auto-pausing on launch For apps with splash screens, you can configure Airship to automatically pause In-App Automation on launch. This prevents In-App Experiences from displaying during the splash screen. Once your app is ready, resume display by setting `isPaused` to `false`. Set the `autoPauseInAppAutomationOnLaunch` option in your Airship config when building `AirshipConfigOptions` (in your Autopilot's `createAirshipConfigOptions` or in `airshipconfig.properties` as `auto_pause_in_app_automation_on_launch = true`): #### Kotlin ```kotlin override fun createAirshipConfigOptions(context: Context) = airshipConfigOptions { // ... other config settings ... // Auto-pause on launch for splash screen setAutoPauseInAppAutomationOnLaunch(true) } ``` When your splash screen is dismissed and the app is ready, resume display: ```kotlin InAppAutomation.shared().isPaused = false ``` #### Java ```java @Override public AirshipConfigOptions createAirshipConfigOptions(Context context) { return AirshipConfigOptions.newBuilder() // ... other config settings ... // Auto-pause on launch for splash screen .setAutoPauseInAppAutomationOnLaunch(true) .build(); } ``` When your splash screen is dismissed and the app is ready, resume display: ```java InAppAutomation.shared().setPaused(false); ``` ### Pausing display Pausing will still allow In-App Experiences to be triggered and queued up for execution, but they will not display. This is useful for preventing in-app experiences from displaying on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. #### Kotlin ```kotlin InAppAutomation.shared().isPaused = true ``` #### Java ```java InAppAutomation.shared().setPaused(true); ``` ### Display interval The display interval controls the amount of time to wait before the manager can display the next triggered In-App Experience. The default value is set to **0 seconds** and can be adjusted to any amount of time in seconds. #### Kotlin ```kotlin InAppAutomation.shared().inAppMessaging.displayInterval = 30 ``` #### Java ```java InAppAutomation.shared().getInAppMessaging().setDisplayInterval(30); ``` ### Controlling per-message display You can control when individual In-App Experiences are ready to display and listen for when they are displayed or finished. This is useful when you need to check app state before displaying content, such as: - Verifying the current Activity is appropriate for the message - Checking custom data in the message's extras (custom keys) to determine if it should display - Ensuring certain app conditions are met before showing the message - Integrating with other in-app messaging products #### Kotlin Set a delegate to control the display. You have access to the message and schedule ID: ```kotlin InAppAutomation.shared().inAppMessaging.displayDelegate = object : InAppMessageDisplayDelegate { override fun isMessageReadyToDisplay(message: InAppMessage, scheduleId: String): Boolean { // Return false to prevent display return false } override fun messageWillDisplay(message: InAppMessage, scheduleId: String) { // Message displayed } override fun messageFinishedDisplaying(message: InAppMessage, scheduleId: String) { // Message finished } } ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (Activity, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```kotlin InAppAutomation.shared().inAppMessaging.notifyDisplayConditionsChanged() ``` #### Java Implement the `InAppMessageDisplayDelegate` to control the display. You have access to the message and schedule ID: **Implement the InAppMessageDisplayDelegate** ```java InAppAutomation.shared().getInAppMessaging().setDisplayDelegate(new InAppMessageDisplayDelegate() { @Override public boolean isMessageReadyToDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Return false to prevent display return false; } @Override public void messageWillDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message displayed } @Override public void messageFinishedDisplaying(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message finished } }); ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (Activity, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```java InAppAutomation.shared().getInAppMessaging().notifyDisplayConditionsChanged(); ``` ## Next steps - Learn how to [create Scenes in the Airship dashboard](https://www.airship.com/docs/guides/messaging/in-app-experiences/scenes/create/) - Present Scene content with [Embedded Content](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/embedded-content/) - Create reusable components with [Custom Views](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/) - Customize [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/in-app-automation/) for IAA # Embedded Content > Integrate Embedded Content into your Android app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content](https://www.airship.com/docs/guides/features/messaging/scenes/embedded-content/). You can set up Embedded Content for Android using Jetpack Compose or XML Views. If you are not already on Android SDK 18.1.4+, see the [Airship Android SDK 17.x to 18.0 migration guide](https://github.com/urbanairship/android-library/blob/18.0.0/documentation/migration/migration-guide-17-18.md). ## Jetpack Compose setup Embedded Content support for Jetpack Compose is provided by an extension library, which must be declared as a dependency of your project. #### Gradle Kotlin **app build.gradle.kts** ```kotlin dependencies { val airshipVersion = "androidSdkVersion" // Other Airship dependencies... implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion") } ``` > **Note:** All Airship dependencies included in the `build.gradle.kts` file should all specify the exact same version. #### Gradle Groovy **app build.gradle** ```groovy dependencies { def airshipVersion = "androidSdkVersion" // Other Airship dependencies... implementation "com.urbanairship.android:urbanairship-automation-compose:$airshipVersion" } ``` > **Note:** All Airship dependencies included in the `build.gradle` file should all specify the exact same version. ### Adding an AirshipEmbeddedView The `AirshipEmbeddedView` is a Composable UI element that defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```kotlin import com.urbanairship.automation.compose.AirshipEmbeddedView @Composable fun HomeScreenBanner() { // Show any "home_banner" Embedded Content AirshipEmbeddedView( embeddedId = "home_banner", modifier = Modifier.fillMaxWidth().height(300.dp) ) } ``` ### Placeholders If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a composable lambda that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior: - If content is available for the `embeddedId`, the `AirshipEmbeddedView` will display it within your composition. - If no content is available for the `embeddedId`, the `AirshipEmbeddedView` will not be visible. - Compose previews will show a default placeholder that displays the `embeddedId`. **Basic integration with placeholder** ```kotlin AirshipEmbeddedView( embeddedId = "home_banner", modifier = Modifier.fillMaxWidth().wrapContentHeight() ) { Text("Placeholder!", Modifier.align(Alignment.Center)) } ``` ### Placing in a Scrolling Container When placed directly in a scrolling Composable, or in a nested Composable within the scrolling parent that is not bounded in the scroll direction, you must provide the parent's size for the corresponding dimension of the embedded view. This enables percent-based sizing to work correctly. A simple way to accomplish this is to use the `onSizeChanged` modifier to store the size of the scrolling parent (or another ancestor) so that the size can be passed to the embedded view via the `parentWidthProvider` or `parentHeightProvider` arguments. **verticalScroll modifier example** ```kotlin val scrollState = rememberScrollState() var parentHeight by remember { mutableIntStateOf(0) } Column( modifier = Modifier.fillMaxSize() .onSizeChanged { parentHeight = it.height } .verticalScroll(scrollState) ) { AirshipEmbeddedView( embeddedId = "home_banner", parentHeightProvider = { parentHeight }, modifier = Modifier.fillMaxWidth() ) // ... } ``` ### Placing in a Lazy Container An approach similar to the [above method](#placing-in-a-scrolling-container) can be used for sizing embedded views inside of Lazy scrolling containers, such as `LazyColumn` or `LazyRow`. It's important to remember to hoist the embedded view state above the Lazy container so that the embedded view can be recycled and re-created properly. You can do this by calling `rememberAirshipEmbeddedViewState` and passing the embedded view ID as an argument, which returns an embedded view state-holder instance for the given `embeddedId`. In the example below, you'll notice that the `AirshipEmbeddedView` call doesn't include an `embeddedId` argument. This is because the `embeddedId` is provided by the remembered `AirshipEmbeddedViewState` instance. **Lazy scrolling example** ```kotlin val lazyListState = rememberLazyListState() // Hoist the embedded state above the LazyColumn. val embeddedViewState = rememberAirshipEmbeddedViewState(embeddedId = "home_banner") var parentHeight by remember { mutableIntStateOf(0) } LazyColumn( state = lazyListState, modifier = Modifier.fillMaxSize() .onSizeChanged { parentHeight = it.height } ) { item { AirshipEmbeddedView( // The embeddedId of "home_banner" from embeddedViewState // will be used by the embedded view. state = embeddedViewState, parentHeightProvider = { parentHeight }, modifier = Modifier.fillMaxWidth() ) } // ... } ``` ## XML Views setup You can use XML Views instead of [Jetpack Compose](#jetpack-compose-setup). ### Adding an AirshipEmbeddedView The `AirshipEmbeddedView` is an Android `View` that defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `airshipEmbeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```xml ``` ### Placeholders If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a reference to an XML layout that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior: - If content is available for the `airshipEmbeddedId`, the `AirshipEmbeddedView` will display it within your layout. - If no content is available for the `airshipEmbeddedId`, the `AirshipEmbeddedView` will not be visible. **Basic integration with placeholder** ```xml ``` ### Placing in a ScrollView or RecyclerView When placed directly in a `ScrollView` or `RecyclerView`, or as a nested child view within a scrolling view that is not bounded in the scroll direction, you must provide the parent's size for the corresponding dimension of the embedded view. This enables percent-based sizing to work correctly. You'll need to determine the container size of the scrolling parent (or another ancestor) and pass the size to the embedded view via the `parentWidthProvider` or `ParentHeightProvider` arguments. **ScrollView example** ```kotlin override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val scrollView = findViewById(R.id.scroll_view) val embeddedView = findViewById(R.id.home_banner_embedded_view) scrollView.doOnPreDraw { embeddedView.parentHeightProvider = { scrollView.height } } } ``` Use with `RecyclerView` is similar to the above example, but you'll need to set the parent size in the `onBindViewHolder` method. One way to accomplish this is to pass the parent size to the adapter so that it can be used when binding the view holder that contains the embedded view. ## Controlling content display order By default, pending Embedded Content is displayed in First In, First Out (FIFO) order per `embeddedId`. If you want to control the order in which pending content is displayed, you can provide a custom `Comparator` to sort the Embedded Content based on fields that you define in the content's extras. #### Compose ```kotlin AirshipEmbeddedView( embeddedId = "home_banner", comparator = { a, b -> // Compare based on the priority field set on the Embedded Content extras. val priorityA = a.extras.opt("priority").getInt(0) val priorityB = b.extras.opt("priority").getInt(0) priorityA.compareTo(priorityB) }, modifier = Modifier.fillMaxWidth() ) ``` A `Comparator` can also be passed as an argument to `rememberAirshipEmbeddedViewState` to control content display order when hoisting the embedded view state. #### XML View ```kotlin override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val embeddedView = findViewById(R.id.home_banner_embedded_view) embeddedView.comparator = Comparator { a, b -> // Compare based on the priority field set on the Embedded Content extras. val priorityA = a.extras.opt("priority").getInt(0) val priorityB = b.extras.opt("priority").getInt(0) priorityA.compareTo(priorityB) } } ``` ## Observing available Embedded Content Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An `AirshipEmbeddedView` will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an `AirshipEmbeddedObserver` to watch for updates. An `AirshipEmbeddedObserver` exposes both a callback and a `Flow` that can be used to receive updates about the availability of Embedded Content. This allows for more dynamic handling of Embedded Content than just content or a placeholder. #### Kotlin **Flow usage** ```kotlin val observer = AirshipEmbeddedObserver("playground") val embeddedInfo = observer.embeddedViewInfoFlow.collectAsState(initial = emptyList()) if (embeddedInfo.value.isEmpty()) { Text("No banner available") } else { Text("Banner available") AirshipEmbeddedView(embeddedId = "home_banner") } ``` #### Java **Callback usage** ```java AirshipEmbeddedObserver observer = new AirshipEmbeddedObserver("home_banner"); observer.setListener(new AirshipEmbeddedObserver.Listener() { @Override public void onEmbeddedViewInfoUpdate(@NonNull List views) { if (views.isEmpty()) { textView.setText("No banner available"); embeddedView.setVisibility(View.GONE); } else { textView.setText("Banner available"); embeddedView.setVisibility(View.VISIBLE); } } }); ``` The `AirshipEmbeddedObserver` can be created to watch for one or more `embeddedId` values or use custom filtering to watch all Embedded Content or a subset determined by inspecting the `embeddedId` or `extras` associated with the Embedded Content. The `embeddedInfos` returned by the callback or `Flow` are in FIFO order, meaning that the first content in the list is the first content that will be displayed. # Custom Views > Register custom Android views with the AirshipCustomViewManager to use them in Scenes. ![Custom View rendered in a Scene on Android](https://www.airship.com/docs/images/custom-views-android_hu_e02177d308b3e6b5.webp) *Custom View rendered in a Scene on Android* To use Custom Views, you must first register the view's name with the `AirshipCustomViewManager`. The name is referenced when adding the Custom View to a Scene. The view manager will call through to the view builders registered for that view's name and provide the properties, name, and some layout hints as arguments. All Custom Views should be registered during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#customizing-airship) to ensure the view is available before a Scene is rendered. #### Kotlin ```kotlin // Return an Android View AirshipCustomViewManager.register("my-custom-view") { context, args -> TextView(context).apply { text = args.properties.optionalField("text") ?: "Fallback" } } // For compose, use a ComposeView AirshipCustomViewManager.register("my-custom-view") { context, args -> ComposeView(context).apply { setContent { MaterialTheme { Text(args.properties.optionalField("text") ?: "Fallback") } } } } ``` #### Java ```java // Return an Android View AirshipCustomViewManager.register("my-custom-view", (context, args) -> { TextView textView = new TextView(context); String text = args.getProperties().optionalField(String.class, "text"); textView.setText(text != null ? text : "Fallback"); return textView; }); // For compose, use a ComposeView AirshipCustomViewManager.register("my-custom-view", (context, args) -> { ComposeView composeView = new ComposeView(context); String text = args.getProperties().optionalField(String.class, "text"); composeView.setContent(content -> { MaterialTheme.INSTANCE.invoke(content, theme -> { Text.INSTANCE.invoke(text != null ? text : "Fallback"); }); }); return composeView; }); ``` ## Example custom view The following example shows a Custom View that renders an embedded map when called to render a Custom View named `map`. In our example, we have `properties` that defines a single `place` field, which is the address of the location that the map should render. #### Kotlin First, define the view handler: ```kotlin /** * Custom View that displays a Google Map with a marker at a specified `place`. * * Roughly based on the [Compose Google Map implementation](https://github.com/googlemaps/android-maps-compose/blob/main/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt) */ class MapCustomViewHandler: AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { val mapView = MapView(context) val lifecycleObserver = MapLifecycleEventObserver(mapView) val onAttachStateListener = object : View.OnAttachStateChangeListener { private var lifecycle: Lifecycle? = null override fun onViewAttachedToWindow(v: View) { lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also { it.addObserver(lifecycleObserver) } } override fun onViewDetachedFromWindow(v: View) { lifecycle?.removeObserver(lifecycleObserver) lifecycle = null lifecycleObserver.moveToBaseState() } } mapView.addOnAttachStateChangeListener(onAttachStateListener) val place: String = args.properties.requireField("place") mapView.getMapAsync { map -> onMapReady(context, map, place) } return mapView } private fun onMapReady(context: Context, map: GoogleMap, place: String) { val geocoder = Geocoder(context) val location = geocoder.getFromLocationName(place, 1).orEmpty() if (location.isNotEmpty()) { val latitude = location[0].latitude val longitude = location[0].longitude val latLng = LatLng(latitude, longitude) // Set up the map with the location map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 10f)) // Optionally, add a marker at the location map.addMarker(MarkerOptions().position(latLng).title(place)) } else { // Show error toast Toast.makeText(context, "Location '$place' not found", Toast.LENGTH_SHORT).show() } } } /** * A [LifecycleEventObserver] that manages the lifecycle of a [MapView]. * * This is used to ensure that the [MapView] is properly managed by the Android lifecycle. */ private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { when (event) { // [mapView.onDestroy] is only invoked from AndroidView->onRelease. Lifecycle.Event.ON_DESTROY -> moveToBaseState() else -> moveToLifecycleState(event.targetState) } } /** * Move down to [Lifecycle.State.CREATED] but only if [currentLifecycleState] is actually above that. * It's theoretically possible that [currentLifecycleState] is still in [Lifecycle.State.INITIALIZED] state. * */ fun moveToBaseState() { if (currentLifecycleState > Lifecycle.State.CREATED) { moveToLifecycleState(Lifecycle.State.CREATED) } } fun moveToDestroyedState() { if (currentLifecycleState > Lifecycle.State.INITIALIZED) { moveToLifecycleState(Lifecycle.State.DESTROYED) } } private fun moveToLifecycleState(targetState: Lifecycle.State) { while (currentLifecycleState != targetState) { when { currentLifecycleState < targetState -> moveUp() currentLifecycleState > targetState -> moveDown() } } } private fun moveDown() { val event = Lifecycle.Event.downFrom(currentLifecycleState) ?: error("no event down from $currentLifecycleState") invokeEvent(event) } private fun moveUp() { val event = Lifecycle.Event.upFrom(currentLifecycleState) ?: error("no event up from $currentLifecycleState") invokeEvent(event) } private fun invokeEvent(event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_RESUME -> mapView.onResume() Lifecycle.Event.ON_PAUSE -> mapView.onPause() Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() else -> error("Unsupported lifecycle event: $event") } currentLifecycleState = event.targetState } } ``` Then register the view: ```kotlin AirshipCustomViewManager.register("map", MapCustomViewHandler()) ``` #### Java First, define the view handler: ```java /** * Custom View that displays a Google Map with a marker at a specified place. */ public class MapCustomViewHandler implements AirshipCustomViewHandler { @Override public View onCreateView(@NonNull Context context, @NonNull AirshipCustomViewArguments args) { MapView mapView = new MapView(context); String place = args.getProperties().requireField(String.class, "place"); mapView.getMapAsync(map -> onMapReady(context, map, place)); return mapView; } private void onMapReady(Context context, GoogleMap map, String place) { Geocoder geocoder = new Geocoder(context); try { List
addresses = geocoder.getFromLocationName(place, 1); if (!addresses.isEmpty()) { Address address = addresses.get(0); LatLng latLng = new LatLng(address.getLatitude(), address.getLongitude()); map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 10f)); map.addMarker(new MarkerOptions().position(latLng).title(place)); } else { Toast.makeText(context, "Location '" + place + "' not found", Toast.LENGTH_SHORT).show(); } } catch (IOException e) { Toast.makeText(context, "Error geocoding: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } } ``` Then register the view: ```java AirshipCustomViewManager.register("map", new MapCustomViewHandler()); ``` ## Scene Control Custom views control their parent scene through the `SceneController`, which is provided in `AirshipCustomViewArguments`. ### Accessing SceneController Access the scene controller via `args.sceneController` in your view handler's `onCreateView` method. #### Kotlin ```kotlin class CustomViewHandler : AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { // Access via args.sceneController args.sceneController.dismiss() return yourView } } ``` #### Java ```java public class CustomViewHandler implements AirshipCustomViewHandler { @Override public View onCreateView(@NonNull Context context, @NonNull AirshipCustomViewArguments args) { // Access via args.getSceneController() args.getSceneController().dismiss(); return yourView; } } ``` ### Dismissing scenes Call `dismiss()` to close the scene, or set `cancelFutureDisplays` to prevent it from displaying again. #### Kotlin ```kotlin // Simple dismiss args.sceneController.dismiss() // Dismiss and cancel future displays args.sceneController.dismiss(cancelFutureDisplays = true) ``` #### Java ```java // Simple dismiss args.getSceneController().dismiss(); // Dismiss and cancel future displays args.getSceneController().dismiss(true); ``` ### Pager navigation Navigate between pages using `navigate()`, which returns `true` if navigation succeeded. #### Kotlin ```kotlin // Navigate forward or backward args.sceneController.pager.navigate(NavigationRequest.NEXT) args.sceneController.pager.navigate(NavigationRequest.BACK) // Check if navigation succeeded val success = args.sceneController.pager.navigate(NavigationRequest.NEXT) ``` #### Java ```java // Navigate forward or backward args.getSceneController().getPager().navigate(NavigationRequest.NEXT); args.getSceneController().getPager().navigate(NavigationRequest.BACK); // Check if navigation succeeded boolean success = args.getSceneController().getPager().navigate(NavigationRequest.NEXT); ``` Observe the pager's `StateFlow` to check current navigation state and enable/disable navigation UI. #### Kotlin ```kotlin // Compose val state = args.sceneController.pager.state.collectAsState() if (state.value.canGoNext) { // Can navigate forward } // View-based lifecycleScope.launch { args.sceneController.pager.state.collect { state -> // Update UI based on state.canGoBack and state.canGoNext } } ``` #### Java ```java LifecycleOwner lifecycleOwner = // get from context CoroutineScope scope = LifecycleScopeKt.getLifecycleScope(lifecycleOwner); FlowKt.launchIn( FlowKt.onEach(args.getSceneController().getPager().getState(), state -> { // Update UI based on state.getCanGoBack() and state.getCanGoNext() return Unit.INSTANCE; }), scope ); ``` ### Sizing management Use `args.sizeInfo` to determine appropriate sizing for your custom view. #### Kotlin ```kotlin // Compose val modifier = when { args.sizeInfo.isAutoWidth && args.sizeInfo.isAutoHeight -> Modifier.wrapContentSize() args.sizeInfo.isAutoWidth -> Modifier.wrapContentWidth().fillMaxHeight() args.sizeInfo.isAutoHeight -> Modifier.fillMaxWidth().wrapContentHeight() else -> Modifier.fillMaxSize() } // View-based val width = if (args.sizeInfo.isAutoWidth) { ViewGroup.LayoutParams.WRAP_CONTENT } else { ViewGroup.LayoutParams.MATCH_PARENT } val height = if (args.sizeInfo.isAutoHeight) { ViewGroup.LayoutParams.WRAP_CONTENT } else { ViewGroup.LayoutParams.MATCH_PARENT } ``` #### Java ```java // View-based int width = args.getSizeInfo().isAutoWidth() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; int height = args.getSizeInfo().isAutoHeight() ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; ``` ![Map Custom View in a Scene on Android](https://www.airship.com/docs/images/custom-views-map-scene-android_hu_9a022a240a253d3a.webp) *Map Custom View in a Scene on Android* ## Embedding Airship Views Airship views like Preference Center can be embedded as custom views. #### Kotlin ```kotlin import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterContent import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterScreen import com.urbanairship.preferencecenter.ui.PreferenceCenterFragment // Compose - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content") { context, args -> val id = args.properties.opt("id").optString() ComposeView(context).apply { setContent { PreferenceCenterContent( identifier = id, modifier = Modifier.fillMaxSize() ) } } } // Compose - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center") { context, args -> val id = args.properties.opt("id").optString() ComposeView(context).apply { setContent { PreferenceCenterScreen( identifier = id, modifier = Modifier.fillMaxSize(), onNavigateUp = { args.sceneController.dismiss() } ) } } } // View - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content") { context, args -> val id = args.properties.opt("id").optString() val activity = context as? FragmentActivity ?: throw IllegalStateException("Context must be FragmentActivity") FrameLayout(context).apply { id = View.generateViewId() activity.supportFragmentManager.beginTransaction() .add(id, PreferenceCenterFragment.create(id)) .commitNow() } } // View - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center") { context, args -> val id = args.properties.opt("id").optString() val activity = context as? FragmentActivity ?: throw IllegalStateException("Context must be FragmentActivity") LinearLayout(context).apply { orientation = LinearLayout.VERTICAL val toolbar = MaterialToolbar(context).apply { setNavigationIcon(R.drawable.abc_ic_ab_back_material) setNavigationOnClickListener { args.sceneController.dismiss() } } addView(toolbar, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val container = FragmentContainerView(context).apply { id = View.generateViewId() } addView(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) activity.supportFragmentManager.beginTransaction() .add(container.id, PreferenceCenterFragment.create(id)) .commitNow() } } ``` #### Java ```java import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterContent; import com.urbanairship.preferencecenter.compose.ui.PreferenceCenterScreen; import com.urbanairship.preferencecenter.ui.PreferenceCenterFragment; // Compose - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content", (context, args) -> { String id = args.getProperties().opt("id").optString(); ComposeView composeView = new ComposeView(context); composeView.setContent(() -> { PreferenceCenterContent(id, Modifier.fillMaxSize(), null); }); return composeView; }); // Compose - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center", (context, args) -> { String id = args.getProperties().opt("id").optString(); ComposeView composeView = new ComposeView(context); composeView.setContent(() -> { PreferenceCenterScreen( id, Modifier.fillMaxSize(), () -> { args.getSceneController().dismiss(); return Unit.INSTANCE; } ); }); return composeView; }); // View - Content only (no navigation bar) AirshipCustomViewManager.register("preference_center_content", (context, args) -> { String id = args.getProperties().opt("id").optString(); FragmentActivity activity = (FragmentActivity) context; FrameLayout layout = new FrameLayout(context); int containerId = View.generateViewId(); layout.setId(containerId); activity.getSupportFragmentManager() .beginTransaction() .add(containerId, PreferenceCenterFragment.create(id)) .commitNow(); return layout; }); // View - Full preference center with navigation bar AirshipCustomViewManager.register("preference_center", (context, args) -> { String id = args.getProperties().opt("id").optString(); FragmentActivity activity = (FragmentActivity) context; LinearLayout layout = new LinearLayout(context); layout.setOrientation(LinearLayout.VERTICAL); MaterialToolbar toolbar = new MaterialToolbar(context); toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material); toolbar.setNavigationOnClickListener(v -> args.getSceneController().dismiss()); layout.addView(toolbar, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); FragmentContainerView container = new FragmentContainerView(context); int containerId = View.generateViewId(); container.setId(containerId); layout.addView(container, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); activity.getSupportFragmentManager() .beginTransaction() .add(containerId, PreferenceCenterFragment.create(id)) .commitNow(); return layout; }); ``` ![Preference Center Custom View in a Scene on Android](https://www.airship.com/docs/images/custom-views-preference-center-scene-android_hu_a81aa6610c793dfd.webp) *Preference Center Custom View in a Scene on Android* # In-App Automation > In-app messages are fully customizable. Minor modifications can be accomplished by overriding the styles, but more advanced customizations can employ an adapter for the given message type. In-App Automation (IAA) powers banner, modal, and fullscreen in-app messages. ## Customization Various options are available for customizing the container view for In-App Automation content via the native SDKs. Scenes are fully customizable in the dashboard and cannot be customized via the SDK. ## Excluding Activities Activities can be excluded from auto-displaying an in-app message. This is useful for preventing in-app message displays on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. **Exclude activity from displaying in-app messages** ```xml ``` ## Listening for Events A listener can be added to the in-app messaging manager to listen for when a message is displayed and finished displaying. This is useful for adding analytics events outside of Airship as well as for further processing of the in-app message. #### Kotlin ```kotlin InAppAutomation.shared().inAppMessaging.displayDelegate = object : InAppMessageDisplayDelegate { override fun isMessageReadyToDisplay(message: InAppMessage, scheduleId: String): Boolean { // Called to check if the message is ready to be displayed return true } override fun messageWillDisplay(message: InAppMessage, scheduleId: String) { // Message displayed } override fun messageFinishedDisplaying(message: InAppMessage, scheduleId: String) { // Message finished } } ``` #### Java ```java InAppAutomation.shared().getInAppMessaging().setDisplayDelegate(new InAppMessageDisplayDelegate() { @Override public boolean isMessageReadyToDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Called to check if the message is ready to be displayed return true; } @Override public void messageWillDisplay(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message displayed } @Override public void messageFinishedDisplaying(@NonNull InAppMessage message, @NonNull String scheduleId) { // Message finished } }); ``` ## Fonts Fonts that are added in XML are available for use with in-app messaging, including downloadable fonts. To add fonts, please read the [Fonts In XML guide](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml). ![Android Custom Font](https://www.airship.com/docs/images/android/android-custom-font_hu_3fb1c1887a6d73ac.webp) *Android Custom Font* After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). Then you can select the stack when [setting in-app message defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ## Styles The Android resource merging feature can be used to override any of the message styles that the SDK provides. Copy any of the styles that need to be overridden into the application's resource directory, then change any of the styles. Styles: - [Banner](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_banner.xml) - [Fullscreen](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_fullscreen.xml) - [Modal](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_modal.xml) - [HTML](https://github.com/urbanairship/android-library/blob/main/urbanairship-automation/src/main/res/values/style_iam_html.xml) ## Custom Adapters Providing an adapter allows full customization for any message type. The adapter will be created by the in-app messaging manager when a message's schedule is triggered. Once created, the adapter will be called to first prepare the in-app message, giving the adapter time to download any resources such as images. After the adapter prepares the message, the adapter will be called to display the message. Be sure to update the adapter factory for the message type you are trying to override. In this example, the adapter is overriding modal messages, therefore the custom display adapter type is `CustomDisplayAdapterType.MODAL`. After the message is displayed, the provided display handler must be notified that the message is finished displaying by returning a `CustomDisplayResolution` via the suspending method or callback. This will allow other in-app messages to be displayed. #### Kotlin ```kotlin class MyCustomDisplayAdapter( private val context: Context, private val message: InAppMessage, private val assets: AirshipCachedAssets, private val scope: CoroutineScope ) : CustomDisplayAdapter.SuspendingAdapter { // Implement the isReady property, used to signal when the adapter is ready to display the message. // If this adapter does not need to wait for anything before displaying the message, you can return // an initial value of true to indicate that it is always ready. Otherwise, set the initial value // to false, and emit true once the adapter is ready to display the message. override val isReady: StateFlow = MutableStateFlow(true) override fun display(context: Context): CustomDisplayResolution { // Display the message... return CustomDisplayResolution.UserDismissed } companion object { fun register(scope: CoroutineScope) { InAppAutomation.shared().inAppMessaging.setAdapterFactoryBlock( type = CustomDisplayAdapterType.MODAL, factoryBlock = { context, message, assets -> MyCustomDisplayAdapter(context, message, assets, scope) } ) } } } ``` #### Java ```java class MyCustomDisplayAdapter implements CustomDisplayAdapter.CallbackAdapter { private final InAppMessage message; public MyCustomDisplayAdapter( Context context, InAppMessage message, AirshipCachedAssets assets ) { this.message = message; } @Override public void display(@NonNull Context context, DisplayFinishedCallback callback) { // Display the message... callback.finished(CustomDisplayResolution.UserDismissed.INSTANCE); } static void register() { InAppAutomation.shared().getInAppMessaging().setAdapterFactoryBlock( CustomDisplayAdapterType.MODAL, (context, message, assets) -> new MyCustomDisplayAdapter(context, message, assets) ); } } ``` #### Kotlin ```kotlin MyCustomDisplayAdapter.register() ``` #### Java ```java MyCustomDisplayAdapter.register(); ``` ## Standard In-App Messages Standard [in-app messages](https://www.airship.com/docs/guides/messaging/messages/content/app/in-app-messages/) delivered through push messages are managed by the legacy in-app message manager. The manager converts the standard in-app message into a new in-app message schedule. The conversion can be customized by setting a builder extender to extend either the schedule builder or the message builder. #### Kotlin ```kotlin InAppAutomation.shared().legacyInAppMessaging .scheduleExtender = { schedule -> // Return an updated schedule schedule.newBuilder() .setPriority(10) .setLimit(3) .build() } ``` #### Java ```java InAppAutomation.shared().getLegacyInAppMessaging() .setScheduleExtender((schedule) -> // Return an updated schedule schedule.newBuilder() .setPriority(10) .setLimit(3) .build() ); ``` #### Kotlin ```kotlin InAppAutomation.shared().legacyInAppMessaging .messageExtender = { message -> val extras = JsonMap.newBuilder() .putAll(message.getExtras()) .put("custom_key", "custom_value") .build() // Return an updated message message.newBuilder() .setExtras(extras) .build() } ``` #### Java ```java InAppAutomation.shared().getLegacyInAppMessaging() .setMessageExtender(message -> { JsonMap extras = JsonMap.newBuilder() .putAll(message.getExtras()) .put("custom_key", "custom_value") .build(); // Return an updated message return message.newBuilder() .setExtras(extras) .build(); }); ``` ## Customizing HTML In-App Messages > **Note:** In order for the Airship JavaScript interface to be loaded into the webview, the URL must be specified in the [UrlAllowList](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#configuring-airship). HTML in-app messages provide a way to display custom content inside a native web view. These types of in-app messages display with a dismiss button built in, but can also be customized to provide their own buttons capable of dismissing the view. Dismissing a view requires calling the dismiss function on the UAirship JavaScript interface with a button resolution object passed in as a parameter. The button resolution object is a JSON object containing information about the interaction type and the button performing the dismissal. It should match the following format: ```javascript { "type" : "button_click", "button_info" : { "id" : "button identifier", "label" : {"text" : "foo"} } } ``` The button resolution requires each of the key fields shown above. These include: - `type` — The type key with the value of resolution type `button_click` - `button_info` — The button info object containing required id and label fields - `id` — The button identifier - `label` — Label object containing the required text key - `text` — The text key with a string value representing the label text Providing a basic dismiss button in HTML: ```html ``` ## Message Center Implement Message Center to provide an inbox for rich HTML-based messages, including display, theming, embedding, and advanced customization. # Message Center > Message Center provides an inbox for rich HTML-based messages that users can view at their convenience, with support for custom theming and display handling. ![Message Center inbox on Android](https://www.airship.com/docs/images/message-center-android.webp) *Message Center inbox on Android* Message Center provides an inbox for rich, HTML-based messages that users can view at their convenience. By default, when your app receives a push notification with a Message Center action, the Message Center automatically displays in a modal activity. The Message Center can also be displayed manually by calling a simple method, making it easy to add a Message Center button to your app's navigation. Message Center inboxes are associated with channel IDs, which persist across app launches, allowing users to access their message history. Airship provides two distinct modules for displaying Message Center on Android, depending on your app's UI framework: - **`urbanairship-message-center-compose`**: Provides Compose-based Message Center UI components, for apps built with Jetpack Compose. - **`urbanairship-message-center`**: Provides XML-based Message Center UI components, for apps using traditional Android Views (XML layouts). You should select only one module based on your UI framework—do not include both modules in the same app. For more information about Message Center messages, see the [Message Center feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Installation Include the correct module for your chosen UI framework: #### Jetpack Compose ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-message-center-compose:$airshipVersion") } ``` #### XML Views ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-message-center:$airshipVersion") } ``` ## Display the Message Center Display the Message Center with a single method call: #### Kotlin ```kotlin Airship.messageCenter.showMessageCenter() ``` #### Java ```java MessageCenter.shared().showMessageCenter(); ``` This displays the Message Center as a modal activity, allowing users to view and manage their messages. When the user closes the Message Center, any changes (such as marking messages as read) are automatically synced with Airship. > **Note:** To embed the Message Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#handling-display-requests) to handle navigation to your embedded Message Center. ## Applying a Custom Theme You can customize the appearance of the Message Center to match your app's style. Android supports theme customization through both Jetpack Compose and XML Views. #### Jetpack Compose To apply a custom theme to the ready-to-use Message Center UI, create a theme and set it on the `MessageCenter` instance early in your app's lifecycle. The overridden `onAirshipReady()` method in your Autopilot class is a good place to do this. **Customizing the theme with Jetpack Compose** ```kotlin // Configure Message Center Theme val messageCenterTheme = MessageCenterTheme( lightColors = MessageCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ), darkColors = MessageCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ), typography = MessageCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) ) // Apply theme to default Message Center UI Airship.messageCenter.theme = messageCenterTheme ``` #### XML Views The ready-to-use Message Center UI uses the `UrbanAirship.MessageCenter` style. You can use xml resource merging to override the default styles, by defining the style in your app. ### Theme Attributes The Message Center supports the following theme attributes: **messageCenterToolbarTitle** : String to use for the Message Center toolbar title **messageCenterIconsEnabled** : Flag to enable message thumbnails in the message list **messageCenterPlaceholderIcon** : The default placeholder image for message thumbnails **messageCenterItemDividersEnabled** : Flag to enable dividers between messages in the list **messageCenterItemDividerInsetStart** : The start inset for message list dividers **messageCenterItemDividerInsetEnd** : The end inset for message list dividers **dividerColor** (set via Material Theme) : The message list divider color, if dividers are enabled **Extending from a Material3 app theme** ```xml ``` > **Note:** If your app doesn't use a `Material3` theme or you need the ability to further customize Message Center styles, the Android resource merging feature can be used to override the default styles that the SDK provides. Copy the [style sheet](https://github.com/urbanairship/android-library/blob/master/urbanairship-message-center/src/main/res/values/style_message_center.xml) > into the application's resource directory, then change any of the styles. ## Working with Messages The Message Center provides methods to fetch, mark as read, and delete messages programmatically. ### Fetch Messages Retrieve messages from the inbox: #### Kotlin ```kotlin // Suspending call scope.launch { val messages = Airship.messageCenter.inbox.getMessages() } // Flow scope.launch { // Collect the messages flow, which emits a new list whenever the inbox is updated Airship.messageCenter.inbox.getMessagesFlow().collect { messages -> // Handle messages } } ``` #### Java ```java PendingResult> messagesResult = MessageCenter.shared().getInbox().getMessagesPendingResult(); messagesResult.addResultCallback(messages -> { // Handle messages }); ``` ### Listen for Message Updates Subscribe to message updates using a listener or Flow: #### Kotlin ```kotlin // Option 1: Messages Flow scope.launch { Airship.messageCenter.inbox.getMessagesFlow().collect { messages -> // Update your UI with the new messages } } // Option 2: InboxListener Airship.messageCenter.inbox.addListener(object: InboxListener { override fun onInboxUpdated() { // Update your UI } }) ``` #### Java ```java MessageCenter.shared().getInbox().addListener(() -> { // Update your UI }); ``` ### Listen for Unread Count Changes Subscribe to unread count updates: #### Kotlin ```kotlin scope.launch { Airship.messageCenter.inbox.getUnreadCountFlow().collect { unreadCount -> // Update badge or UI } } ``` #### Java ```java MessageCenter.shared().getInbox().getUnreadCountPendingResult() .addResultCallback(unreadCount -> { // Update badge or UI }); ``` ### Refresh Messages Manually refresh the message list from the server: #### Kotlin ```kotlin Airship.messageCenter.inbox.fetchMessages { success -> // Handle result } ``` #### Java ```java MessageCenter.shared().getInbox().fetchMessages(new Inbox.FetchMessagesCallback() { @Override public void onFinished(boolean success) { // Handle the result } }); ``` ### Mark Messages as Read Mark one or more messages as read: #### Kotlin ```kotlin Airship.messageCenter.inbox.markMessagesRead(messageId) ``` #### Java ```java MessageCenter.shared().getInbox().markMessagesRead("messageId"); ``` ### Delete Messages Delete one or more messages: #### Kotlin ```kotlin Airship.messageCenter.inbox.deleteMessages("messageId") ``` #### Java ```java MessageCenter.shared().getInbox().deleteMessages("messageId"); ``` ## Filter Messages by Named User By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, set up filtering in your custom Message Center implementation. See [Message Center Filtering](https://www.airship.com/docs/developer/sdk-integration/android/message-center/embedding/#message-center-filtering) in the Embedding guide. When creating Message Center messages, include a custom key with `named_user_id` as the key and the user's actual ID as the value: - **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject). - **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide. ### Filtering Behavior With named user filtering enabled: - If you target `User A` in a message while they are logged in, the message appears in their inbox. - If you target `User B` in a message while they are logged in, the message appears in their inbox. - If you target `User A` or `User B` while the other is logged in, the message does not appear. - If you target `User A` or `User B` while neither is logged in, the message does not appear. # Embed the Message Center > Embed the Message Center directly in your app's navigation, create custom implementations with full control over design and functionality, and filter messages by named user. This guide covers how to embed the Message Center directly in your app's navigation instead of displaying it as an overlay, create custom implementations, and filter messages. ## Handling Display Requests To use a custom Message Center implementation or navigate to your embedded Message Center instead of the default overlay activity, set a listener to handle display requests: #### Kotlin ```kotlin Airship.messageCenter.setOnShowMessageCenterListener { messageId: String? -> // Navigate to your custom Message Center UI // messageId is optional - null means show the full message list // Return true to prevent the default SDK display true } ``` #### Java ```java MessageCenter.shared().setOnShowMessageCenterListener(messageId -> { // Navigate to your custom Message Center UI // messageId is optional - null means show the full message list // Return true to prevent the default SDK display return true; }); ``` ## Embedding with Jetpack Compose When embedding Message Center composables, you can choose between an all-in-one screen with a customizable top bar and a content-only Message Center view, without a top bar. Both composables must be wrapped in a `MessageCenterTheme`, which allows the theme to be customized. **MessageCenterScreen** ```kotlin MessageCenterTheme { MessageCenterScreen() } ``` **MessageCenterContent** ```kotlin MessageCenterTheme { MessageCenterContent() } ``` **Customizing the theme** ```kotlin val lightColors = MessageCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ) val darkColors = MessageCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ) val typography = MessageCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) MessageCenterTheme( colors = if (isSystemInDarkTheme()) darkColors else lightColors, typography = typography ) { // MessageCenterScreen OR MessageCenterContent } ``` ## Embedding with XML Views The Message Center UI can be embedded in any `FragmentActivity` or `Fragment` using `MessageCenterFragment`. When embedding the MessageCenterFragment, either use a `FragmentContainerView` or create the fragment directly. ### Using FragmentContainerView Add the `MessageCenterFragment` directly in your layout XML: ```xml ``` ### Creating Fragment Programmatically Alternatively, create and add the fragment programmatically: #### Kotlin ```kotlin val fragment = MessageCenterFragment.newInstance() ``` #### Java ```java MessageCenterFragment fragment = MessageCenterFragment.newInstance(); ``` You will need to [set up display request handling](#handling-display-requests) to navigate to your embedded fragment instead of letting Airship launch the `MessageCenterActivity`. ### Integrating with Navigation For more control over the UI, `MessageCenterListFragment` and `MessageCenterMessageFragment` can be used to embed the list and message views separately, each maintaining its own `Toolbar`. This example assumes that Jetpack Navigation is being used to navigate between the list and message views, but any navigation method can be used. ### Custom Message List Fragment ```kotlin class CustomMessageListFragment() : MessageCenterListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById(R.id.toolbar) toolbar.inflateMenu(messageCenterR.menu.ua_message_center_list_pane_menu) // Set up the toolbar, if desired. setupWithNavController(toolbar, findNavController()) onMessageClickListener = OnMessageClickListener { // Handle message clicks by navigating to the message fragment // (or replace with custom navigation logic). findNavController().navigate( R.id.action_messageCenterFragment_to_messageFragment, bundleOf( MessageCenterMessageFragment.ARG_MESSAGE_ID to it.id, MessageCenterMessageFragment.ARG_MESSAGE_TITLE to it.title ) ) } } } ``` ### Custom Message Fragment ```kotlin class CustomMessageFragment : MessageCenterMessageFragment(R.layout.fragment_inbox_message) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) toolbar?.run { // Inflate the default menu inflateMenu(messageCenterR.menu.ua_message_center_message_pane_menu) // Set up the toolbar, if desired. setupWithNavController(toolbar, findNavController(view)) } // Handle message deletion from the message view onMessageDeletedListener = OnMessageDeletedListener { // Handle message deletion by navigating back to the message list fragment // (or replace with custom navigation logic). findNavController().popBackStack() // Optionally show a toast confirmation message context?.run { val msg = getQuantityString(messageCenterR.plurals.ua_mc_description_deleted, 1, 1) Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() } } } } ``` These custom fragments can then be embedded into your app UI using `FragmentContainerView` or by using `FragmentManager` programmatically. You will need to [set up display request handling](#handling-display-requests) to navigate to your custom message list fragment instead of letting Airship launch the `MessageCenterActivity`. ## Layout Support The Message Center provides automatic support for both single-pane and two-pane layouts, when on large display sizes or foldable devices. In two-pane mode, the selected message is highlighted in the list and displayed in the detail pane. Right-to-left (RTL) layout is also supported when `android:supportsRtl="true"` is set in your application manifest. ## Message Center Filtering Sometimes it can be useful to filter the contents of the Message Center according to some predetermined pattern. To facilitate this, use the shared `MessageCenter` instance to set a predicate. Once set, only messages that match the predicate will be displayed. ### Filter by Named User Filter messages to show only those for the current named user: #### Kotlin ```kotlin MessageCenter.shared().predicate = Predicate { message -> val namedUserID = Airship.shared().contact.namedUserID if (namedUserID == null) { return@Predicate false } // Check if message has matching named_user_id in extras val extras = message.extras val messageNamedUserID = extras?.get("named_user_id") as? String messageNamedUserID == namedUserID } ``` #### Java ```java MessageCenter.shared().setPredicate(message -> { String namedUserID = Airship.shared().getContact().getNamedUserID(); if (namedUserID == null) { return false; } // Check if message has matching named_user_id in extras Map extras = message.getExtras(); String messageNamedUserID = extras != null ? extras.get("named_user_id") : null; return messageNamedUserID != null && messageNamedUserID.equals(namedUserID); }); ``` ### Custom Filtering Create custom predicates for any filtering logic: #### Kotlin ```kotlin MessageCenter.shared().predicate = Predicate { message -> // Example: Only show messages with "cool" in the title message.title.contains("cool") } ``` #### Java ```java MessageCenter.shared().setPredicate(message -> // Example: Only show messages with "cool" in the title message.getTitle().contains("cool") ); ``` ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Key Components **MessageCenter** : The main entry point for fetching messages and handling callbacks. Access via `MessageCenter.shared()`. **Inbox** : Provides an interface for retrieving messages asynchronously and accessing the local message array. Access via `MessageCenter.shared().inbox`. > **Note:** The message list uses a local database. Message objects are refreshed with the list. Don't hold onto individual message instances indefinitely. **Message** : Model object representing an individual message. Instances don't contain the message body—they point to authenticated URLs that should be displayed in a webview. **Display Callbacks** : Set `MessageCenter.shared().setOnShowMessageCenterListener()` to handle when messages should be displayed. ## Preference Center Implement Preference Centers to allow users to manage their subscription preferences, including display, theming, and embedding options. # Preference Center > Display Preference Centers using the Airship UI, which automatically handles user preferences and syncs with the Airship backend. ![Preference Center on Android](https://www.airship.com/docs/images/preference-center-android_hu_84f8445eff5c8fa8.webp) *Preference Center on Android* > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. The Preference Center feature allows users to manage their subscription list preferences as configured in the Airship Dashboard. Airship provides two distinct modules for displaying Preference Centers on Android, depending on your app's UI framework: - **`urbanairship-preference-center-compose`**: Provides Compose-based Preference Center UI components, for apps built with Jetpack Compose. - **`urbanairship-preference-center`**: Provides XML-based Preference Center UI components, for apps using traditional Android Views (XML layouts). You should select only one module based on your UI framework—do not include both modules in the same app. For more information about configuring Preference Centers, see the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Installation Include the correct module for your chosen UI framework: #### Jetpack Compose ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-preference-center-compose:$airshipVersion") } ``` #### XML Views ```kotlin dependencies { val airshipVersion = "androidSdkVersion" implementation("com.urbanairship.android:urbanairship-preference-center:$airshipVersion") } ``` ## Displaying a Preference Center Display a Preference Center with a single method call. The Preference Center will be launched in its own Activity over your app, allowing users to manage their subscription preferences, and automatically syncing changes with Airship. #### Kotlin ```kotlin Airship.preferenceCenter.open("my-first-pref-center") ``` #### Java ```java PreferenceCenter.shared().open("my-first-pref-center"); ``` > **Note:** To embed the Preference Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/android/preference-center/embedding/). You can also [intercept display requests](#override-default-display-behavior) to handle navigation to your embedded Preference Center. ## Applying a Custom Theme You can customize the appearance of the Preference Center to match your app's style. Android supports theme customization through both Jetpack Compose and XML Views. #### Jetpack Compose To apply a custom theme to the ready-to-use Preference Center UI, create a theme and set it on the `PreferenceCenter` instance early in your app's lifecycle. The overridden `onAirshipReady()` method in your Autopilot class is a good place to do this. **Customizing the theme with Jetpack Compose** ```kotlin // Configure Preference Center Theme val preferenceCenterTheme = PreferenceCenterTheme( lightColors = PreferenceCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ), darkColors = PreferenceCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ), typography = PreferenceCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) ) // Apply theme to default Preference Center UI Airship.preferenceCenter.theme = preferenceCenterTheme ``` #### XML Views The ready-to-use Preference Center UI uses the `UrbanAirship.PreferenceCenter` style. You can use xml resource merging to override the default styles, by defining the style in your app. **Extending from a Material3 app theme** ```xml ``` If your app doesn't use a `Material3` theme or you need the ability to further customize preference center styles, The Android resource merging feature can be used to override the default styles that the SDK provides. Copy the [style sheet](https://github.com/urbanairship/android-library/blob/master/urbanairship-preference-center/src/main/res/values/style_preference_center.xml) into the application's resource directory, then change any of the styles. # Embed the Preference Center > Embed the Preference Center view directly in your app's navigation instead of displaying it as an overlay. This guide covers advanced Preference Center customization options, from styling the default UI to creating fully custom implementations. ## Handling Display Requests To use a custom Preference Center implementation or navigate to your embedded Preference Center instead of the default activity, set a listener to handle showing your custom UI: #### Kotlin Set the `PreferenceCenterOpenListener` during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). ```kotlin Airship.preferenceCenter.openListener = object : PreferenceCenter.OnOpenListener { override fun onOpenPreferenceCenter(preferenceCenterId: String): Boolean { // Navigate to custom preference center UI // true to prevent default behavior // false for default Airship handling return true } } ``` #### Java Set the `PreferenceCenterOpenListener` during the [onAirshipReady callback](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). ```java PreferenceCenter.shared().setOpenListener(preferenceCenterId -> { // Navigate to custom preference center UI // true to prevent default behavior // false for default Airship handling return true; }); ``` ## Embedding with Jetpack Compose When embedding Preference Center composables, you can choose between an all-in-one screen with a customizable top bar and a content-only Preference Center view, without a top bar. Both composables must be wrapped in a `PreferenceCenterTheme`, which allows the theme to be customized. **PreferenceCenterScreen** ```kotlin PreferenceCenterTheme { PreferenceCenterScreen(identifier = "my-first-pref-center") } ``` **PreferenceCenterContent** ```kotlin PreferenceCenterTheme { PreferenceCenterContent(identifier = "my-first-pref-center") } ``` **Customizing the theme** ```kotlin val lightColors = PreferenceCenterColors.lightDefaults( background = Color(0xDEDEDE), surface = Color(0xFFFFFF), accent = Color(0x6200EE), ) val darkColors = PreferenceCenterColors.darkDefaults( background = Color(0x121212), surface = Color(0x1E1E1E), accent = Color(0xBB86FC), ) val typography = PreferenceCenterTypography.defaults( fontFamily = FontFamily(context.resources.getFont(R.font.roboto_regular)) ) PreferenceCenterTheme( colors = if (isSystemInDarkTheme()) darkColors else lightColors, typography = typography ) { // PreferenceCenterScreen OR PreferenceCenterContent } ``` ## Embedding with XML Views When embedding the PreferenceCenterFragment, either use a [FragmentContainerView](https://developer.android.com/reference/androidx/fragment/app/FragmentContainerView) or create the fragment directly. You must specify the ID of the Preference center to be displayed when creating the fragment. The static `create` on `PreferenceCenter` will handle passing the given id to the fragment as an argument: #### Kotlin ```kotlin val fragment = PreferenceCenterFragment.create(preferenceCenterId = "my-first-pref-center") ``` #### Java ```java PreferenceCenterFragment fragment = PreferenceCenterFragment.create("my-first-pref-center"); ``` You will need to [override the open behavior](#override-default-display-behavior) to navigate to the embedded fragment instead of letting Airship launch the `PreferenceCenterActivity`. ## Audience Management Integrate audience management features into your app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs, listen for channel creation, and configure the channel capture tool. Each device or app install generates a unique identifier known as the Channel ID. Once a Channel ID is created, it persists in the application until the app is reinstalled or its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. #### Kotlin ```kotlin val channelId = Airship.channel.id ``` #### Java ```java String channelId = Airship.getChannel().getId(); ``` The SDK creates the Channel ID asynchronously, so it may not be available immediately on the first run. The SDK automatically batches and applies changes to Channel data when the Channel is created, so you do not need to wait for the Channel to be available before modifying data. Applications that need to access the Channel ID can use a listener to receive notification when it becomes available. #### Kotlin Using `channelIdFlow` (StateFlow): ```kotlin // channelIdFlow is a StateFlow that emits the channel ID when it's created scope.launch { Airship.channel.channelIdFlow.collect { channelId -> channelId?.let { Log.d("Sample", "Channel created: $it") } } } ``` Using `addChannelListener`: ```kotlin Airship.channel.addChannelListener { channelId -> Log.d("Sample", "Channel created: $channelId") } ``` #### Java Using `addChannelListener`: ```java Airship.getChannel().addChannelListener(new AirshipChannelListener() { @Override public void onChannelCreated(@NonNull String channelId) { // created } }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfigOptions`, see [Android SDK Setup](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). #### Kotlin ```kotlin val options = airshipConfigOptions { // ... setChannelCaptureEnabled(false) } ``` #### Java ```java AirshipConfigOptions options = AirshipConfigOptions.newBuilder() // ... .setChannelCaptureEnabled(false) .build(); ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during initialization. For more information about Privacy Manager, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/android/data-collection/privacy-manager/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) You can call `identify` multiple times with the same Named User ID. The SDK automatically deduplicates `identify` calls made with the same Named User ID. If you change the ID from a previous value, the SDK automatically dissociates the Contact from the previous Named User ID. #### Kotlin ```kotlin Airship.contact.identify("some named user ID") ``` #### Java ```java Airship.getContact().identify("some named user ID"); ``` If the user logs out of the device, you may want to reset the contact. Resetting clears any anonymous data and dissociates the contact from the Named User ID, if set. Call this method only when the user manually logs out of the app. Otherwise, you cannot target the Channel by its Contact data. #### Kotlin ```kotlin Airship.contact.reset() ``` #### Java ```java Airship.getContact().reset(); ``` You can retrieve the Named User ID only if you set it through the SDK. #### Kotlin ```kotlin Airship.contact.namedUserId ``` #### Java ```java Airship.getContact().getNamedUserId(); ``` ### Email channel association When an email address is registered through the SDK, it will be registered for both transactional and commercial emails by default. To change this behavior, you can override the options to request [[Double Opt-In](https://www.airship.com/docs/reference/glossary/#double_opt_in)](https://www.airship.com/docs/developer/api-integrations/email/getting-started/#double-opt-in) for commercial messages. #### Kotlin ```kotlin val properties = JsonMap.newBuilder().put("place", "paris").build() val options = EmailRegistrationOptions.commercialOptions(commercialDate, transactionalDate, properties) Airship.contact.registerEmail("your@example.com", options) ``` #### Java ```java JsonMap properties = JsonMap.newBuilder().put("place", "paris").build(); EmailRegistrationOptions options = EmailRegistrationOptions.commercialOptions(commercialDate, transactionalDate, properties); Airship.getContact().registerEmail("your@example.com", options); ``` ### SMS channel association When an [MSISDN](https://www.airship.com/docs/reference/glossary/#msisdn) is registered through the SDK, Airship sends a message to that number, prompting them to opt in. For more information, see the SMS platform documentation: [Non-Mobile Double Opt-In](https://www.airship.com/docs/developer/api-integrations/sms/opt-in-out-handling/#non-mobile-double-opt-in). #### Kotlin ```kotlin val options = SmsRegistrationOptions.options("senderId") Airship.contact.registerSms("yourMsisdn", options) ``` #### Java ```java SmsRegistrationOptions options = SmsRegistrationOptions.options("senderId"); Airship.getContact().registerSms("yourMsisdn", options); ``` ### Open Channel association Open Channels support notifications to any medium that can accept a JSON payload, through either the Airship API or web dashboard. For more information about Open Channels, see the [Open Channels documentation](https://www.airship.com/docs/developer/api-integrations/open/getting-started/). #### Kotlin ```kotlin val options = OpenChannelRegistrationOptions.options("platformName") Airship.contact.registerOpenChannel("address", options) ``` #### Java ```java OpenChannelRegistrationOptions options = OpenChannelRegistrationOptions.options("platformName"); Airship.getContact().registerOpenChannel("address", options); ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. #### Kotlin ```kotlin Airship.channel.editTags { addTag("some_tag") removeTag("some_other_tag") } // Accessing channel tags val tags = Airship.channel.tags ``` #### Java ```java Airship.getChannel().editTags() .addTag("some_tag") .removeTag("some_other_tag") .apply(); // Accessing channel tags ArrayList tags = Airship.getChannel().getTags(); ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Kotlin ```kotlin Airship.channel.editTagGroups { addTag("loyalty", "bronze-member") removeTag("loyalty", "bronze-member") setTag("games", "bingo") } ``` #### Java ```java Airship.getChannel().editTagGroups() .addTag("loyalty", "bronze-member") .removeTag("loyalty", "bronze-member") .setTag("games", "bingo") .apply(); ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Kotlin ```kotlin Airship.contact.editTagGroups { addTag("loyalty", "bronze-member") removeTag("loyalty", "bronze-member") setTag("games", "bingo") } ``` #### Java ```java Airship.getContact().editTagGroups() .addTag("loyalty", "bronze-member") .removeTag("loyalty", "bronze-member") .setTag("games", "bingo") .apply(); ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. #### Kotlin ```kotlin Airship.channel.editAttributes { setAttribute("device_name", "Bobby's Phone") setAttribute("average_rating", 4.99) removeAttribute("vip_status") } ``` #### Java ```java Airship.getChannel().editAttributes() .setAttribute("device_name", "Bobby's Phone") .setAttribute("average_rating", 4.99) .removeAttribute("vip_status") .apply(); ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. #### Kotlin ```kotlin Airship.contact.editAttributes { setAttribute("first_name", "Bobby") setAttribute("birthday", Date(524300400000)) } ``` #### Java ```java Airship.getContact().editAttributes() .setAttribute("first_name", "Bobby") .setAttribute("birthday", Date(524300400000)) .apply(); ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. You can set and remove JSON Attributes on a Channel or a Contact. #### Kotlin ```kotlin Airship.contact.editAttributes { setAttribute( attribute = "attribute_name", instanceId = "instance_id", expiration = Date(), json = jsonMapOf( "key" to "value", "another_key" to "another_value" ) ) } ``` ## Verifying Attributes To verify that attributes are set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. Search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. #### Kotlin ```kotlin // Modifying channel subscription lists Airship.channel.editSubscriptionLists { subscribe("food") unsubscribe("sports") } // Fetching channel subscription lists val channelSubscriptions = Airship.channel.fetchSubscriptionLists() ``` #### Java ```java // Modifying channel subscription lists Airship.getChannel().editSubscriptionLists() .subscribe("food") .unsubscribe("sports") .apply(); // Fetching channel subscription lists PendingResult> channelSubscriptions = Airship.getChannel().fetchSubscriptionListsPendingResult(); ``` ## Contact Subscription Lists Contact subscriptions are set at the user level and require a Channel scope that specifies the types to which the subscription list applies. #### Kotlin ```kotlin // Modifying contact subscription lists Airship.contact.editSubscriptionLists { subscribe("food", "app") unsubscribe("sports", "sms") } // Fetching contact subscription lists val contactSubscriptions = Airship.contact.fetchSubscriptionLists() ``` #### Java ```java // Modifying contact subscription lists Airship.getContact().editSubscriptionLists() .subscribe("food", "app") .unsubscribe("sports", "sms") .apply(); // Fetching contact subscription lists PendingResult>> contactSubscriptions = Airship.getContact().fetchSubscriptionListsPendingResult(); ``` ## Verifying Subscription Lists To verify that subscription lists are set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. Search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ## Data Collection Overview of data collection and controls provided by the Airship SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: #### Kotlin | Privacy Manager Flag | Kotlin Constant | AirshipConfig Value | |----------------------|--------------------------------------------|---------------------| | Push | PrivacyManager.Feature.PUSH | push | | In-App Automation | PrivacyManager.Feature.IN_APP_AUTOMATION | in_app_automation | | Message Center | PrivacyManager.Feature.MESSAGE_CENTER | message_center | | Tags and Attributes | PrivacyManager.Feature.TAGS_AND_ATTRIBUTES | tags_and_attributes | | Contacts | PrivacyManager.Feature.CONTACTS | contacts | | Feature Flags | PrivacyManager.Feature.FEATURE_FLAGS | feature_flags | | Analytics | PrivacyManager.Feature.ANALYTICS | analytics | | All | PrivacyManager.Feature.ALL | all | | None | PrivacyManager.Feature.NONE | none | #### Java | Privacy Manager Flag | Java Constant | AirshipConfig Value | |----------------------|--------------------------------------------|---------------------| | Push | PrivacyManager.Feature.PUSH | push | | In-App Automation | PrivacyManager.Feature.IN_APP_AUTOMATION | in_app_automation | | Message Center | PrivacyManager.Feature.MESSAGE_CENTER | message_center | | Tags and Attributes | PrivacyManager.Feature.TAGS_AND_ATTRIBUTES | tags_and_attributes | | Contacts | PrivacyManager.Feature.CONTACTS | contacts | | Feature Flags | PrivacyManager.Feature.FEATURE_FLAGS | feature_flags | | Analytics | PrivacyManager.Feature.ANALYTICS | analytics | | All | PrivacyManager.Feature.ALL | all | | None | PrivacyManager.Feature.NONE | none | ## Configuring default enabled features Default enabled features can be set in the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfigOptions`, see [Android SDK Setup](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/). #### Kotlin **AirshipConfigOptions** ```kotlin airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.PUSH) } ``` **airshipconfig.properties** ```properties enabledFeatures = push ``` #### Java **AirshipConfigOptions** ```java AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.PUSH) .build(); ``` **airshipconfig.properties** ```properties enabledFeatures = push ``` To fully disable data collection by default, set the enabled features to none. #### Kotlin **AirshipConfigOptions** ```kotlin airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.NONE) } ``` **airshipconfig.properties** ```properties enabledFeatures = none ``` #### Java **AirshipConfigOptions** ```java AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.NONE) .build(); ``` **airshipconfig.properties** ```properties enabledFeatures = none ``` ## Enabling features at runtime You can enable or disable features at runtime based on user consent: #### Kotlin ```kotlin // Initially disable all features val options = airshipConfigOptions { // ... setEnabledFeatures(PrivacyManager.Feature.NONE) } // Later, when user grants consent: Airship.privacyManager.enableFeatures( PrivacyManager.Feature.PUSH, PrivacyManager.Feature.ANALYTICS ) ``` #### Java ```java // Initially disable all features AirshipConfigOptions options = AirshipConfigOptions.newBuilder() // ... .setEnabledFeatures(PrivacyManager.Feature.NONE) .build(); // Later, when user grants consent: Airship.getPrivacyManager().enableFeatures( PrivacyManager.Feature.PUSH, PrivacyManager.Feature.ANALYTICS ); ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section - [Analytics](https://www.airship.com/docs/developer/sdk-integration/android/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Permission Prompts > Request additional system permissions (e.g., location) from users using Opt-in Actions. The Airship SDK automatically handles push notification permissions. For additional permissions like location, you can use Opt-in Actions to prompt users using native permission prompts. Opt-in Actions are a special type of [Action](https://www.airship.com/docs/reference/glossary/#action) that are handled by `PermissionsManager`. For an overview of all supported actions and where they are available, see the [Actions](https://www.airship.com/docs/guides/messaging/messages/actions/) guide. ## Supported Opt-in Types * Push — Handled automatically by the SDK (no implementation needed) * Location — Requires implementing a custom `PermissionDelegate` ## Implementing Location Opt-in To implement Location Opt-in, create a custom `PermissionDelegate` and register it with `PermissionsManager` to handle location permissions. ### Create a Location Permission Delegate #### Kotlin ```kotlin val delegate = SinglePermissionDelegate(Manifest.permission.ACCESS_COARSE_LOCATION) ``` For fine location, use `Manifest.permission.ACCESS_FINE_LOCATION` instead. #### Java ```java SinglePermissionDelegate delegate = new SinglePermissionDelegate(Manifest.permission.ACCESS_COARSE_LOCATION); ``` For fine location, use `Manifest.permission.ACCESS_FINE_LOCATION` instead. ### Register the Permission Delegate After creating a location `PermissionDelegate`, register it with `PermissionsManager` [after Airship is ready](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/#customizing-airship): #### Kotlin ```kotlin Airship.permissionsManager .setPermissionDelegate(Permission.LOCATION, delegate) ``` #### Java ```java Airship.getPermissionsManager() .setPermissionDelegate(Permission.LOCATION, delegate); ``` ## Troubleshooting Common issues and solutions for Airship SDK setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/android/installation/getting-started/) or [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/android/installation/advanced-integration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Initialization errors The Airship SDK fails during initialization or behaves unexpectedly in these cases: - `takeOff` has already been successfully called. - `takeOff` was called but the SDK was not configured through Autopilot or `airshipconfig.properties`. - Autopilot or `airshipconfig.properties` has invalid or missing credentials. - Required dependencies are missing from the `build.gradle` file. - `takeOff` was called before the application context was available. ## takeOff called multiple times If `takeOff` throws because it has already been successfully called, verify the following: - `takeOff` is only called once per app launch. - It's not called in both `Autopilot` and `Application.onCreate()`. ## Credential validation During initialization, the SDK checks only that credentials are present and correctly formatted in code, through `Autopilot`, or in `airshipconfig.properties`. It does not verify that the credentials are valid against Airship servers. If the configuration is valid and you initialize the SDK only once, initialization can complete without reporting an error even when the credentials themselves are invalid. The credentials the SDK uses at initialization, whether you call `takeOff` yourself or configure the SDK through `Autopilot` or `airshipconfig.properties`, are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. **Symptoms of missing or invalid credentials:** - No channel ID appears in the logs - Warnings or errors in the logs after initialization - Channel is not created in the Airship dashboard - Push notifications are not received If you are experiencing credential issues, do the following: 1. Compare your Airship project credentials with the values in your app, either in code, through `Autopilot`, or in `airshipconfig.properties`. - Credentials must match the expected format and character set. - Credentials must not be empty strings or contain extra whitespace. 1. Ensure both `productionAppKey`/`productionAppSecret` and `developmentAppKey`/`developmentAppSecret` are set in code, through `Autopilot`, or in `airshipconfig.properties` before the SDK initializes. **Guidelines for credentials:** - Use development credentials for development builds and production credentials for release builds. - Configure both development and production credentials in your app, either in code, through `Autopilot`, or in `airshipconfig.properties`. The SDK chooses which to use based on your build configuration. ## airshipconfig.properties not found or invalid If the SDK cannot load or parse `airshipconfig.properties`, or the file is invalid, verify the following: - The file exists in your app's `src/main/assets/` directory - The file name is exactly `airshipconfig.properties` - All required keys are present: `productionAppKey`, `productionAppSecret`, `developmentAppKey`, `developmentAppSecret` - The file format is valid (it must be in standard Java properties format) - The file is included in your build output ## FCM configuration issues If push notifications are not working or you need to confirm your FCM integration, verify the following: - The `google-services.json` file is configured for your app and Firebase project - The FCM dependency is included in your `build.gradle` file - The Google Services plugin is applied in your `build.gradle` file - The Firebase project is set up correctly in the Firebase Console # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/android/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. The SDK provides detailed information about their current state. ## Get Current Notification Status Read the current notification status from `Airship.push.pushNotificationStatus` to inspect each field: #### Kotlin ```kotlin val status = Airship.push.pushNotificationStatus Log.d("Airship", "User notifications enabled: ${status.isUserNotificationsEnabled}") Log.d("Airship", "Notifications allowed: ${status.areNotificationsAllowed}") Log.d("Airship", "Privacy feature enabled: ${status.isPushPrivacyFeatureEnabled}") Log.d("Airship", "Push token registered: ${status.isPushTokenRegistered}") Log.d("Airship", "User opted in: ${status.isUserOptedIn}") Log.d("Airship", "Fully opted in: ${status.isOptIn}") ``` #### Java ```java PushNotificationStatus status = Airship.getPush().getPushNotificationStatus(); Log.d("Airship", "User notifications enabled: " + status.isUserNotificationsEnabled()); Log.d("Airship", "Notifications allowed: " + status.getAreNotificationsAllowed()); Log.d("Airship", "Privacy feature enabled: " + status.isPushPrivacyFeatureEnabled()); Log.d("Airship", "Push token registered: " + status.isPushTokenRegistered()); Log.d("Airship", "User opted in: " + status.isUserOptedIn()); Log.d("Airship", "Fully opted in: " + status.isOptIn()); ``` ## Listen for Status Changes Use the following to monitor notification status changes in real time: #### Kotlin ```kotlin scope.launch { Airship.push.pushNotificationStatusFlow.collect { status -> Log.d("Airship", "Notification status changed:") Log.d("Airship", "User opted in: ${status.isUserOptedIn}") Log.d("Airship", "Fully opted in: ${status.isOptIn}") } } ``` #### Java ```java Airship.getPush().addNotificationStatusListener(status -> { Log.d("Airship", "Notification status changed:"); Log.d("Airship", "User opted in: " + status.isUserOptedIn()); Log.d("Airship", "Fully opted in: " + status.isOptIn()); }); ``` ## Understanding Notification Status Fields The `NotificationStatus` class provides detailed information about why push might not be working: | Field | Description | |-------|-------------| | `isUserNotificationsEnabled` | Whether `pushManager.userNotificationsEnabled` is set to `true` | | `areNotificationsAllowed` | Whether the user has granted notification permissions | | `isPushPrivacyFeatureEnabled` | Whether the push privacy feature is enabled in `PrivacyManager` | | `isPushTokenRegistered` | Whether a push token has been successfully registered with FCM | | `isUserOptedIn` | `true` if user notifications are enabled, privacy feature is enabled, and notifications are allowed | | `isOptIn` | `true` if `isUserOptedIn` is `true` AND a push token is registered | ## Common Status Scenarios **Status:** `isUserNotificationsEnabled = false` - **Cause:** `pushManager.userNotificationsEnabled` has not been set to `true`. - **Solution:** Enable user notifications in your app code. **Status:** `areNotificationsAllowed = false` - **Cause:** User denied notification permissions or permissions not yet requested. (Android 13+) - **Solution:** Request notification permissions or guide user to system settings. **Status:** `isPushPrivacyFeatureEnabled = false` - **Cause:** Push privacy feature is disabled in Privacy Manager. - **Solution:** Enable the push privacy feature: `UAirship.shared().privacyManager.setEnabledFeatures(PrivacyManager.Feature.PUSH)`. **Status:** `isPushTokenRegistered = false` - **Cause:** Device hasn't received a push token from FCM yet. - **Solution:** Check network connectivity, FCM configuration, and device/emulator limitations. **Status:** `isUserOptedIn = true` but `isOptedIn = false` - **Cause:** Push token registration is pending or failed. - **Solution:** Check Logcat for FCM registration errors, verify network connectivity, and ensure proper FCM setup.