[
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: 🐞 Bug report\ndescription: File a Bug Report for unexpected or incorrect SDK Behavior\ntitle: '[Bug]: '\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report! \n        \n        :warning: Please consider contacting [Braze Support](https://www.braze.com/docs/support_contact) for in-depth troubleshooting and to avoid leaking private information to our public Github issues.\n  - type: input\n    id: sdk_version\n    attributes:\n      label: Braze Android SDK Version\n      placeholder: ex. 15.0.1\n    validations:\n      required: true\n  - type: textarea\n    id: repro_steps\n    attributes:\n      label: Steps To Reproduce\n      description: Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)\n      placeholder: |\n        Example:\n          ```\n          \n          ```\n    validations:\n      required: true\n  - type: textarea\n    id: expected_behavior\n    attributes:\n      label: Expected Behavior\n      description: What did you expect to happen?\n    validations:\n      required: true\n  - type: textarea\n    id: actual_behavior\n    attributes:\n      label: Actual Incorrect Behavior\n      description: What incorrect behavior happened instead?\n    validations:\n      required: true\n  - type: textarea\n    id: verbose_logs\n    attributes:\n      label: Verbose Logs\n      description: Enable [Verbose Logging](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/initial_sdk_setup/additional_customization_and_configuration/#android-verbose-logging). This will be automatically formatted into code, so no need for backticks.\n      render: shell\n  - type: textarea\n    id: other_info\n    attributes:\n      label: Additional Information\n      description: Anything else you'd like to share?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Braze Support\n    url: https://www.braze.com/docs/support_contact\n    about: Contact Braze Support for company or campaign-specific troubleshooting\n  - name: Security Issues\n    url: https://www.braze.com/docs/developer_guide/disclosures/security_and_vulnerability_disclosure/\n    about: Please report security vulnerabilities here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: ✅ Feature Request\ndescription: Request New SDK Features\ntitle: '[Feature]: '\nlabels: [\"feature-request\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        :point_right: You can also submit feature requests in our [Public Roadmap Portal](https://dashboard.braze.com/resources/roadmap)\n  - type: textarea\n    id: problem\n    attributes:\n      label: What problem are you facing?\n      description: Help us understand what you're unable to accomplish, or what's difficult with your integration\n      placeholder: |\n        ex: I am unable to accomplish XYZ today, since the SDK does not allow me to...\n    validations:\n      required: true\n  - type: textarea\n    id: workarounds\n    attributes:\n      label: Workarounds\n      description: Are there any workarounds you can use? How complicated are they?\n    validations:\n      required: true\n  - type: textarea\n    id: ideal_solution\n    attributes:\n      label: Ideal Solution\n      description: What would your ideal solution look like?\n    validations:\n      required: false\n  - type: textarea\n    id: other_information\n    attributes:\n      label: Other Information\n      description: Any additional information you'd like to share?\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nThank you for opening an issue with Braze!\n\nIf you are experiencing issues with our product, please consider directing your issue to support@braze.com, as that is the best channel for solving integration issues. Please see the bottom for more explanation.\n\nFor other items, like requests for modifications to our SDK or bug reports, please use the following format:\n-->\n\n* [ ] I have checked [Braze's documentation](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/initial_sdk_setup/android_sdk_integration/) and am still experiencing the issue.\n* [ ] I have an ANR related issue and have filled out the \"ANR Reporting\" section below.\n\n# Standard Reporting\n\n## What version of the Braze Android SDK are you using?\n\n<!--\nExample: 3.0.1\n-->\n\n## What did you do?\n\n<!--\nPlease replace this with how you've integrated and customized the Braze SDK.\nExample: Integrated Braze with the lifecycle listener and used automatic push registration.\n-->\n\n## What did you expect to happen?\n\n<!--\nPlease replace this with the expected behavior.\nExample: The Braze SDK is integrated properly and creating sessions.\n-->\n\n## What happened instead?\n\n<!--\nPlease replace this with the actual behavior.  \nExample: The Braze SDK doesn't create any sessions for the test user.\n-->\n\n## Steps to reproduce\n\n<!--\nPlease give us detailed steps so we can reproduce the issue on our end. This is very important and will help speed up the investigation.\nExample:\n- Start the app\n- Change to new user\n- Set a custom in-app message listener\n- Log a custom event\n- In-app message doesn't appear\n-->\n\n## Are you doing any feature customizations that may relate to the issue? Can you share the code snippet?\n\n<!--\nPlease provide any information or code snippets that can help us understand or reproduce the issue.\n-->\n\n## Project that demonstrates the issue\n\n<!--\nPlease link to a project we can download that reproduces the issue.\n-->\n\n<!--\nNote: We recommend e-mailing support@braze.com with any integration issues, as our team actively optimizes this support channel to provide efficient and robust resolutions. Integration issues also often require logs and other information that may contain private data, and our main support channel ensures no risk of leaking sensitive data.\n-->\n\n# ANR Reporting\n<!--\nPlease go over https://developer.android.com/topic/performance/vitals/anr for more information on ANR reports.\n-->\n\n## Entire ANR Report\n<!--\nPlease provide your entire, unmodified ANR report here\n-->\n\n## Braze Related Threads In ANR\n<!--\nPlease list all Thread IDs (or just Thread traces) in the above ANR report that contain Braze SDK code.\n\nCode from the Braze SDK usually is in the `com.appboy` or `bo.app` package, depending on your Proguard configuration.\n-->\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by http://www.gitignore.io\n\n### Intellij ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm\n\n## Directory-based project format\n.idea/\n\n## File-based project format\n*.iml\n\n## Additional for IntelliJ\nout/\n\n### Android ###\n*.keystore\n\n# Files for the Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\n\n# built application files\n*.apk\n*.ap_\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n.DS_Store\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# :warning: This repository has been permanently moved to [https://github.com/braze-inc/braze-android-sdk](https://github.com/braze-inc/braze-android-sdk/blob/master/CHANGELOG.md)\n\n## 24.3.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v24.3.0)\n\n##### Fixed\n- Fixed an issue where the SDK would attempt to to access the visual service WindowManager from non-visual contexts, resulting in benign StrictMode errors.\n- Added `@JvmStatic` to `com.braze.push.BrazeHuaweiPushHandler.handleHmsRemoteMessageData()`.\n- Fixed an issue where notification extra data was not being passed along in Push Story main image clicks.\n- Fixed an issue where ContentCardAdapter was not properly handling bad indexes being passed in.\n- Fixed an issue where a user's push subscription state would not change to \"opted in\" upon accepting the Android 13+ push prompt.\n\n##### Added\n- Added the ability to configure dismissal of Push Stories on click by adding `BrazeConfig.setDoesPushStoryDismissOnClick()` or `<bool name=\"com_braze_does_push_story_dismiss_on_click\">true</bool>` to your `braze.xml`. Defaults to true.\n\n## 24.2.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v24.2.0)\n\n##### Added\n- Added support for the upcoming Braze Feature Flags product.\n\n##### Changed\n- Changed the default behavior for images to more aggressively sample large images.\n  - Images will be sampled until their effective bitmap size (i.e. W x H x 4 bytes) is below 16 MB.\n  - Images will be sampled until both (and not either) the half-width and half-height of the image is less than or equal to the image destination dimensions.\n- Changed the behavior of failed Content Card requests to automatically retry on server 500 errors and SDK Authentication errors.\n\n## 24.1.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v24.1.0)\n\n##### Added\n- Added `BrazeActivityLifecycleCallbackListener.registerOnApplication()` which allows for registering the lifecycle callback listener from any `Context`.\n\n## 24.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v24.0.0)\n\n#### Breaking\n- Location and geofence functionality has moved to a new module called `com.braze:android-sdk-location`. Add this module to your `build.gradle` if you are using Braze location functionality.\n- Deprecated classes starting with `Appboy` have now been removed.\n- Moved `com.appboy` packages to `com.braze`.\n- All xml classes and values in them have been changed from `appboy` to `braze`. All custom code should be updated accordingly.\n- `BrazeNotificationUtils.isAppboyPushMessage()` removed. Please use instead:\n  - Java: `BrazeNotificationUtils.isBrazePushMessage(Intent)`\n  - Kotlin: `Intent.isBrazePushMessage()`\n- `APPBOY_NOTIFICATION_OPENED_SUFFIX`, `APPBOY_NOTIFICATION_RECEIVED_SUFFIX`, and `APPBOY_NOTIFICATION_DELETED_SUFFIX` are removed.\n  - Instead, please use `Braze.getInstance(context).subscribeToPushNotificationEvents()`\n- Updated the minimum version of `com.google.android.gms:play-services-location` required for Braze Geofences to `20.0.0`.\n\n##### Added\n- Added the ability to optionally pipe Braze logcat from `BrazeLogger` to a custom callback via `BrazeLogger.onLoggedCallback`.\n  ```kotlin\n    BrazeLogger.onLoggedCallback = fun(priority: BrazeLogger.Priority, message: String, throwable: Throwable?) {\n      // Custom callback logic here\n    }\n  ```\n  ```java\n    BrazeLogger.setOnLoggedCallback((priority, s, throwable) -> {\n      // Custom logic here\n      return null;\n    });\n  ```\n\n##### Changed\n- Removed `BrazeUser.setFacebookData()` and `BrazeUser.setTwitterData()`.\n- Changed the default behavior of `DefaultContentCardsUpdateHandler` to use the creation time vs last update time when sorting Content Cards.\n\n## 23.3.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.3.0)\n\n##### Fixed\n- Fixed the behavior of the Braze HTML In-App Message bridge method `requestPushPermission()` to not cause the in-app message to reload.\n- Fixed `com.braze.ui.inappmessage.views.InAppMessageImageView` to guard against null values of `InAppMessageImageView.inAppRadii`.\n\n##### Changed\n- Removed `com.appboy.ui.inappmessage.IInAppMessageViewWrapperFactory`. Please use `com.braze.ui.inappmessage.IInAppMessageViewWrapperFactory`.\n- Changed `com.braze.ui.inappmessage.views.InAppMessageFullView.getMessageClickableView` to be nullable.\n\n## 23.2.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.2.1)\n\n##### Fixed\n- Fixed the fields of `DefaultInAppMessageViewWrapper` to be `open`, allowing them to be subclassed in Kotlin properly.\n\n## 23.2.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.2.0)\n\n##### Fixed\n- Fixed the fields of `DefaultInAppMessageViewWrapper` to be `protected`, allowing them to be subclassed.\n- Fixed `BrazeNotificationPayload` and `BrazePushReceiver` to not hold onto an Activity context for longer than needed.\n\n##### Added\n- Added a config field `BrazeConfig.setIsHtmlInAppMessageApplyWindowInsetsEnabled()` to configure the SDK to automatically apply window insets to HTML In-App messages.\n  - By default, this value is false.\n- Added [`subscribeToNoMatchingTriggerForEvent`](https://appboy.github.io/appboy-android-sdk/kdoc/braze-android-sdk/com.braze/-braze/subscribe-to-no-matching-trigger-for-event.html) which is called if no Braze in-app message was triggered for a given event.\n\n##### Changed\n- Removed `com.appboy.ui.inappmessage.listeners.IInAppMessageWebViewClientListener`. Please use `com.braze.ui.inappmessage.listeners.IInAppMessageWebViewClientListener`.\n- Removed `AppboyInAppMessageHtmlBaseView.APPBOY_BRIDGE_PREFIX`. Please use `InAppMessageHtmlBaseView.BRAZE_BRIDGE_PREFIX`.\n\n## 23.1.2\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.1.2)\n\n##### Changed\n- Removed the use of the Kotlin Coroutines method `limitedParallelism()`.\n\n## 23.1.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.1.1)\n\n##### Fixed\n- Fixed the `DefaultInAppMessageViewWrapper` to be Kotlin open, allowing it to be subclassed.\n\n## 23.1.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.1.0)\n\n##### Added\n- Added more reliable HTML In-App Message focusing specifically for TV environments. To use this behavior please set `com.braze.configuration.BrazeConfig.Builder.setIsTouchModeRequiredForHtmlInAppMessages` to `false`.\n- Added `BrazeNotificationPayload.extras` as a `Map<String, String>` to easily retrieve dashboard provided KVPs for push notification data.\n- Added support for Content Cards to evaluate Retry-After headers.\n\n## 23.0.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.0.1)\n\n##### Fixed\n- Fixed an issue where `BaseCardView` would sometimes have the wrong size for a given image.\n\n##### Changed\n- Added proguard rules to keep `enum.values()` and `enum.valueOf(String)` for users who don't use the default Android proguard rules.\n\n## 23.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v23.0.0)\n\n#### Breaking\n- `BaseContentCardView.bindViewHolder()` now takes `Card` instead of generic type.\n\n##### Fixed\n- Fixed an issue where apps with a target of Android 12 running on Android 13 devices would not automatically create a default notification channel upon a push notification being received.\n\n##### Added\n- Added ability to retrieve deeplinks from `BrazeNotificationPayload` objects via `BrazeNotificationPayload().deeplink`.\n\n## 22.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v22.0.0)\n\n#### Breaking\n- `Appboy.java` is now `Braze.kt`. Kotlin clients will need to update their code to support the use of Kotlin properties on the Braze singleton where needed.\n  - `Braze.registerPushToken()`/`Braze.getRegisteredPushToken()` is now `Braze.setRegisteredPushToken()`/`Braze.getRegisteredPushToken()`. If using Kotlin, use the property `Braze.registeredPushToken`.\n  - `Braze.getDeviceId` is now just `Braze.deviceId` for Kotlin.\n  - `Braze.enableMockNetworkAppboyRequestsAndDropEventsMode` is now `Braze.enableMockNetworkRequestsAndDropEventsMode()`.\n  - `Appboy.java` has been removed. For example, calls like `Appboy.getInstance()` will need to be `Braze.getInstance()` moving forward.\n  - Replaced `setCustomAppboyNotificationFactory()` with `setCustomBrazeNotificationFactory() / customBrazeNotificationFactory`.\n  - Renamed `enableMockAppboyNetworkRequestsAndDropEventsMode` to `enableMockNetworkRequestsAndDropEventsMode`.\n- Moved `com.appboy.IBrazeEndpointProvider` to `com.braze.IBrazeEndpointProvider`.\n- Renamed `com.appboy.events.IEventSubscriber` to `com.braze.events.IEventSubscriber`.\n- Removed `Appboy.registerAppboyPushMessages() / Appboy.getAppboyPushMessageRegistrationId()`. Replaced with `getRegisteredPushToken() / setRegisteredPushToken()`.\n- Replaced `IAppboyNotificationFactory` with `IBrazeNotificationFactory`.\n\n##### Fixed\n- Fixed an issue in `BrazePushReceiver` where eager In-App Message test displays and Content Card serializations from push notifications wouldn't work unless notifications were enabled on the device.\n- Fixed an issue where devices between the API 19 up to API 29 would not perform automatic data syncs in some cases.\n- Fixed an issue where carryover in-app messages wouldn't display on subsequent Views on new Activities.\n- Fixed an issue where some long running In-App Message HTML WebViews would call View methods on non UI threads.\n\n##### Added\n- Added `IBraze.subscribeToPushNotificationEvents()` to allow for subscriptions to push notification events without the use of a `BroadcastReceiver`.\n  - Recommended to be placed in your `Application.onCreate()`.\n\n##### Changed\n- Changed `com.braze.models.outgoing.BrazeProperties.clone()` to return `BrazeProperties?`.\n\n## 21.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v21.0.0)\n\n##### Important\n- This release includes support for Android 13 (Tiramisu / API 33).\n\n#### Breaking\n- Removed `IAppboy.logContentCardsDisplayed`. This method was not part of the recommended Content Cards integration and can be safely removed.\n\n##### Changed\n- Changed target API for the SDK to 33.\n\n## 20.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v20.0.0)\n\n#### Breaking\n- Changed `BrazeNotificationStyleFactory` to remove deprecated functions.\n  - Removed `BrazeNotificationStyleFactory.getBigNotificationStyle(Context, Bundle, Bundle, NotificationCompat.Builder)`. Use `BrazeNotificationStyleFactory.getNotificationStyle(NotificationCompat.Builder, BrazeNotificationPayload)` instead.\n  - Removed `BrazeNotificationStyleFactory.getBigTextNotificationStyle(BrazeConfigurationProvider, Bundle)`. Use `BrazeNotificationStyleFactory.getBigTextNotificationStyle(BrazeNotificationPayload)` instead.\n  - Removed `BrazeNotificationStyleFactory.getStoryStyle(Context, Bundle, Bundle, NotificationCompat.Builder)`. Use `BrazeNotificationStyleFactory.getStoryStyle(NotificationCompat.Builder, BrazeNotificationPayload)` instead.\n- Changed `BrazeNotificationActionUtils` to remove deprecated functions.\n  - Removed `BrazeNotificationActionUtils.addNotificationActions(Context, NotificationCompat.Builder, Bundle)`. Use `BrazeNotificationActionUtils.addNotificationActions(NotificationCompat.Builder, BrazeNotificationPayload)` instead.\n  - Removed `BrazeNotificationActionUtils.addNotificationAction(Context, NotificationCompat.Builder, Bundle, Int)`. Use `BrazeNotificationActionUtils.addNotificationAction(BrazeNotificationPayload.ActionButton)` instead.\n  - Removed `AppboyNotificationActionUtils`. Use `BrazeNotificationActionUtils` instead.\n- Removed `AppboyHuaweiPushHandler`. Use `BrazeHuaweiPushHandler` instead.\n- Removed `AppboyFirebaseMessagingService`. Use `BrazeFirebaseMessagingService` instead.\n- Removed `AppboyAdmReceiver`. Use `BrazeAmazonDeviceMessagingReceiver` instead.\n- `BrazeFirebaseMessagingService.handleBrazeRemoteMessage()` and `BrazeFirebaseMessagingService.isBrazePushNotification()` now require non-null parameters.\n- `UriAction.channel` is now `Channel.CONTENT_CARD` for actions that originate from a Content Card instead of `Channel.NEWS_FEED`.\n\n##### Fixed\n-  Fixed an issue that would prevent SDK Authentication errors from being retried.\n\n##### Added\n- Modified `BrazeProperties.addProperties()` to allow adding nested properties via `JSONObject` or `Map<String, *>`.\n- Added support for Braze Action Deeplink Click Actions.\n\n##### Changed\n- Slideup messages now have a maximum width of 450dp. This can be adjusted by modifying `@dimen/com_braze_inappmessage_slideup_max_width`.\n- Added `com.braze.Constants` with constants starting with \"BRAZE_\" that replace the corresponding \"APPBOY_\" constants in `com.appboy.Constants`. The \"APPBOY_\" constants are deprecated and will be removed in a future release.\n\n## 19.0.0\n\n##### Important\n- It is highly recommended to include the compiler flag `-Xjvm-default=all` in your Gradle build options due to the new use of default arguments in the SDK. Without this flag, you may see a compiler warning about \"Inheritance from an interface with '@JvmDefault' members\". An example is included below:\n\n```groovy\n  android {\n    kotlinOptions {\n      freeCompilerArgs = ['-Xjvm-default=all']\n      jvmTarget = \"1.8\"\n    }\n  }\n```\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v19.0.0)\n\n##### ⚠ Breaking\n- Modified behavior of `BrazeProperties(JSONObject)` when `Date` is part of JSONObject.\n  - Previously, Date objects in the JSONObject would be converted with the `Date.toString()` (e.g. \"Thu Jan 01 03:15:33 CST 1970\").\n  - Date objects in the JSONObject are now converted to `BrazeDateFormat.LONG` (e.g. \"1970-01-01 09:15:33\"). This behavior is consistent with `BrazeProperties.addProperty(Date)`.\n- Converted `IInAppMessage` to Kotlin and changed several methods to no longer allow for null inputs or return boolean statuses on field setters.\n  - `IInAppMessage.setClickAction()` is renamed to `setClickBehavior()` and now returns void.\n  - `MessageButton.setClickAction()` is renamed to `setClickBehavior()` and now returns void.\n  - `InAppMessageImmersiveBase.setMessageButtons()` no longer accepts null. Pass in an empty list to clear.\n- Converted `Card` to Kotlin, so JVM signatures may have changed.\n  - Removed `Card.isEqualToCard()`. Please use `card.equals(otherCard)` instead.\n  - Removed `Card.isRead()` and `Card.setIsRead()`. Please use `Card.isIndicatorHighlighted` (Kotlin) or `Card.isIndicatorHighlighted()` and `Card.setIndicatorHighlighted()` (Java).\n- Removed `com.appboy.AnimationUtils`, `com.appboy.ViewUtils`, `com.appboy.UriUtils`, `com.appboy.IAction`, `com.appboy.NewsfeedAction` and `com.appboy.UriAction` classes. The Braze namespaced classes remain.\n- `BrazeDeeplinkHandler.createUriActionFromUrlString()` and `BrazeDeeplinkHandler.createUriActionFromUri()` now require non-null values for uri/url and channel.\n  - `AppboyNavigator` has been removed in favor of `BrazeDeeplinkHandler`.\n- Removed `AppboyNotificationUtils` in favor of `BrazeNotificationUtils`.\n- Removed `AppboyLifecycleCallbackListener`. Please use `BrazeActivityLifecycleCallbackListener`.\n  - Removed `BrazeLifecycleCallbackListener.setInAppMessagingRegistrationBlacklist()` in favor of `BrazeLifecycleCallbackListener.setInAppMessagingRegistrationBlocklist()`. Removed `BrazeLifecycleCallbackListener.setSessionHandlingBlacklist()` in favor of `BrazeLifecycleCallbackListener.setSessionHandlingBlocklist()`.\n- Removed `AppboyContentCardsManager`. Please use `BrazeContentCardsManager` instead.\n- Removed `AppboyEmptyContentCardsAdapter`. Please use `EmptyContentCardsAdapter` instead.\n- Removed `BrazeUser.setAvatarImageUrl(String)`.\n\n##### Fixed\n- Fixed the startup behavior of the SDK to not perform caller thread blocking operations when setting up SharedPreferences and other disk reading I/O.\n- Fixed a potential issue where the default implementation of `Webview.onRenderProcessGone()` could lead to app crashes. Thanks to @ankitsingh08 for finding the issue.\n\n##### Changed\n- Added `BrazeProperties(Map<String, *>)` constructor.\n- Changed `Appboy.getConfiguredApiKey()` to accept a `BrazeConfigurationProvider` instead of a `Context` object.\n- Deprecated `AppboyBootReceiver`. Please use `BrazeBootReceiver` instead.\n- Deprecated `APPBOY_WEBVIEW_URL_EXTRA`. Please use `BRAZE_WEBVIEW_URL_EXTRA` instead.\n- Changed the SDK to not wake the screens of `Configuration.UI_MODE_TYPE_TELEVISION` devices when receiving push notifications.\n  - These screen types will not be awoken even if `isPushWakeScreenForNotificationEnabled()` is true and the permission `Manifest.permission.WAKE_LOCK` is granted.\n  - Special thanks to @IanGClifton for https://github.com/Appboy/appboy-android-sdk/pull/213.\n\n## 18.0.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v18.0.1)\n\n##### Fixed\n- Fixed an issue introduced in 17.0.0 where some HTML In-App Message zip asset files containing hidden `__MACOSX` folders without a corresponding entry for that folder would cause the in-app message to fail to display.\n\n## 18.0.0\n\n##### Important\n- It is highly recommended to include the compiler flag `-Xjvm-default=all` in your Gradle build options due to the new use of default arguments in the SDK. Without this flag, you may see a compiler warning about \"Inheritance from an interface with '@JvmDefault' members\". An example is included below:\n\n```groovy\n  android {\n    kotlinOptions {\n      freeCompilerArgs = ['-Xjvm-default=all']\n      jvmTarget = \"1.8\"\n    }\n  }\n```\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v18.0.0)\n\n> This version has a known issue with HTML In-App Message which was fixed in v18.0.1\n \n##### ⚠ Breaking\n- Removed `AppboyLruImageLoader` in favor of `DefaultBrazeImageLoader`.\n  - `com.appboy.lrucache.AppboyLruImageLoader` -> `com.braze.images.DefaultBrazeImageLoader`.\n  - `com.appboy.Appboy.getAppboyImageLoader` -> `com.appboy.Appboy.getImageLoader`.\n  - `com.appboy.Appboy.setAppboyImageLoader` -> `com.appboy.Appboy.setImageLoader`.\n- Removed `IAppboyEndpointProvider` in favor of `IBrazeEndpointProvider`.\n    - If using `Braze.setAppboyEndpointProvider()` please use `Braze.setEndpointProvider()`.\n   \n##### Fixed\n- Fixed an issue introduced in 15.0.0 where Full in-app messages on tablets may have had an incorrect background color.\n\n##### Added\n- Added the ability to change SDK authentication signature with `Braze.changeUser()` when the current user id and a new signature is passed in.\n    - Previously, `Braze.changeUser()` would not change the SDK authentication signature if the current user id was used.\n\n##### Changed\n- `InAppMessageCloser` is deprecated.\n  - Use `BrazeInAppMessageManager.hideCurrentlyDisplayingInAppMessage()` to hide currently displayed in-app messages.\n  - Use `IInAppMessage.setAnimateOut()` to set whether your in-app message should animate on close.\n  - New version of `IInAppMessageManagerListener.onInAppMessageClicked()` and `IInAppMessageManagerListener.onInAppMessageButtonClicked()` that don't use `InAppMessageCloser` have been added.\n    - If you override the deprecated functions that use `InAppMessageCloser`, those will be called.\n    - If you override the new functions and don't override the deprecated functions, the new functions will be called.\n- Deprecated `ContentCardsUpdatedEvent.getLastUpdatedInSecondsFromEpoch`.\n    - Use `getTimestampSeconds()` (Java) or `timestampSeconds` (Kotlin).\n\n## 17.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v17.0.0)\n\n> This version has a known issue with HTML In-App Message which was fixed in v18.0.1\n\n##### ⚠ Breaking\n- `BrazeLogger.setLogLevel()` replaced with direct property setter `BrazeLogger.logLevel` for Kotlin.\n- Removed `AppboyLogger, com.appboy.IntentUtils, com.appboy.StringUtils` class. The Braze namespaced classes remain.\n- Removed `com_braze_locale_api_key_map` as a configuration option and `BrazeConfig.setLocaleToApiMapping()`. If you need to change your API key based on locale, please use `BrazeConfig` at runtime instead.\n\n##### Added\n- Added `Braze.isDisabled()` to determine whether the SDK is disabled.\n- Added `Braze.addSdkMetadata()` to allow self reporting of SDK Metadata fields via the `BrazeSdkMetadata` enum.\n  - Fields may also be added via a `string-array` to your `braze.xml` with the key `com_braze_sdk_metadata`. The allowed items are the same as the keys found in the `BrazeSdkMetadata` enum. For example when using Branch:\n  ```xml\n    <string-array name=\"com_braze_sdk_metadata\">\n     <item>BRANCH</item>\n    </string-array>\n  ```\n  - Fields are additive across all reporting methods.\n\n## 16.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v16.0.0)\n\n##### ⚠ Breaking\n- Removed `AppboyConfigurationProvider` in favor of `BrazeConfigurationProvider`.\n  - Any deprecated usages, such as in the `IBrazeNotificationFactory` have also been removed.\n\n##### Fixed\n- Fixed an issue introduced in 13.1.0 where session start location updates would fail to update on pre API 30 devices.\n- Fixed an issue introduced in 13.1.0 where geofence update events would fail to update properly.\n\n##### Added\n- Added the ability to namespace all `braze.xml` configurations to be able to use `braze` in place of `appboy`. The Braze namespaced configuration keys will take precedence over the `appboy` keys.\n  - For example, `com_appboy_api_key` can be replaced with `com_braze_api_key`.\n  - Be sure to look for and update any API keys in your build variants as the `com_braze_api_key` from your default variant might take precedence unexpectedly.\n  - All `com_appboy_*` configuration keys in XML will be removed in a future release so it is advised to migrate these configuration keys to their `com_braze_*` counterparts.\n\n##### Changed\n- Changed target API for the SDK to 31.\n\n## 15.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v15.0.0)\n\n##### Important\n- It is highly recommended to do extensive QA after updating to this release, especially for clients doing any amount of Content Card or In-App Message customizations.\n\n##### ⚠ Breaking\n- All Content Cards layout/drawables/colors/dimens identifiers containing `com_appboy_content_cards`/`com_appboy_content_card` were replaced with `com_braze_content_cards`/`com_braze_content_card` respectively.\n  - Content Card drawables `icon_pinned, icon_read, icon_unread` are now `com_braze_content_card_icon_pinned, com_braze_content_card_icon_read, com_braze_content_card_icon_unread`.\n- All In-App Message layout/drawables/colors/dimens identifiers containing `com_appboy_inappmessage`/`com_appboy_in_app_message` replaced with `com_braze_inappmessage`.\n- All styles under namespace `Appboy.*` moved to `Braze.*`.\n  - Any `Appboy.*` style overrides must be migrated to `Braze.*` as there is no backwards compatibility.\n  - For example, a style override for `Appboy.Cards.ImageSwitcher` must be renamed to `Braze.Cards.ImageSwitcher`.\n- Several classes/interfaces have been moved to a Braze namespace/package.\n  - In-App Messages\n    - In-App Message classes under `com.appboy.models.*` moved to `com.braze.models.inappmessage`\n    - Class `com.appboy.ui.inappmessage.InAppMessageCloser` -> `com.braze.ui.inappmessage.InAppMessageCloser`\n    - Enum `com.appboy.ui.inappmessage.InAppMessageOperation` -> `com.braze.ui.inappmessage.InAppMessageOperation`\n    - Enums in package `com.appboy.enums.inappmessage.*` moved to `com.braze.enums.inappmessage`\n  - Content Cards\n    - Interface `IContentCardsUpdateHandler` moved to `com.braze.ui.contentcards.handlers.IContentCardsUpdateHandler`\n    - Interface `IContentCardsViewBindingHandler` moved to `com.braze.ui.contentcards.handlers.IContentCardsViewBindingHandler`\n    - Interface `AppboyContentCardsActionListener` moved to `com.braze.ui.contentcards.listeners.DefaultContentCardsActionListener`\n    - Classes in package `com.appboy.ui.contentcards.view.*` moved to `com.braze.ui.contentcards.view.*`\n      - This is the package containing all Content Card default views.\n    - Class `com.appboy.events.ContentCardsUpdatedEvent` -> `com.braze.events.ContentCardsUpdatedEvent`\n  - Miscellaneous\n    - Class `AppboyBaseFragmentActivity` moved to `com.braze.ui.activities.BrazeBaseFragmentActivity`\n- Removed deprecated `IInAppMessageManagerListener#onInAppMessageReceived` from `IInAppMessageManagerListener`.\n- Removed `AppboyUser` in favor of `BrazeUser`.\n  - Note that for Kotlin consumers, `Appboy.currentUser?` and `Braze.currentUser?` are valid due to the removal of generics on the `Braze.getCurrentUser()` method.\n\n##### Added\n- Added support for Conversational Push.\n- Added the ability for custom broadcast receivers to not require the host package name as a prefix when declaring intent filters in your app manifest.\n  - `<action android:name=\"${applicationId}.intent.APPBOY_PUSH_RECEIVED\" />` should be replaced with `<action android:name=\"com.braze.push.intent.NOTIFICATION_RECEIVED\" />`\n  - `<action android:name=\"${applicationId}.intent.APPBOY_NOTIFICATION_OPENED\" />` should be replaced with `<action android:name=\"com.braze.push.intent.NOTIFICATION_OPENED\" />`\n  - `<action android:name=\"${applicationId}.intent.APPBOY_PUSH_DELETED\" />` should be replaced with `<action android:name=\"com.braze.push.intent.NOTIFICATION_DELETED\" />`\n  - The `appboy` intents have been deprecated but are still available. They will be removed in a future release so migrating early is highly recommended.\n  - Both the `appboy` and `braze` intents are sent for backwards compatibility so only one set should be registered at a time.\n- Added `BrazeUser.addToSubscriptionGroup()` and `BrazeUser.removeFromSubscriptionGroup()` to add or remove a user from an email or SMS subscription group.\n  - Added `brazeBridge.getUser().addToSubscriptionGroup()` and `brazeBridge.getUser().removeFromSubscriptionGroup()` to the javascript interface for HTML In-App Messages.\n\n##### Changed\n- Several classes in the android-sdk-ui artifact have been renamed to the Braze namespace/package. Whenever possible, the original classes are still available. However, they will be removed in a future release so migrating early is highly recommended.\n  - Classes in package `com.appboy.push.*` moved to `com.braze.push.*`\n  - Classes in package `com.appboy.ui.inappmessage.views` moved to `com.braze.ui.inappmessage.views`\n  - Classes in package `com.appboy.ui.inappmessage.listeners` moved to `com.braze.ui.inappmessage.listeners`\n  - Interfaces in `com.appboy.ui.inappmessage.*` moved to `com.braze.ui.inappmessage.*`\n  - Class `com.appboy.AppboyFirebaseMessagingService` -> `com.braze.push.BrazeFirebaseMessagingService`\n  - Class `com.appboy.AppboyAdmReceiver` -> `com.braze.push.BrazeAmazonDeviceMessagingReceiver`\n  - Class `com.appboy.ui.AppboyContentCardsFragment` -> `com.braze.ui.contentcards.ContentCardsFragment`\n  - Class `com.appboy.ui.activities.AppboyContentCardsActivity` -> `com.braze.ui.activities.ContentCardsActivity`\n  - Class `com.appboy.ui.AppboyWebViewActivity` -> `com.braze.ui.BrazeWebViewActivity`\n  - Class `com.appboy.ui.inappmessage.AppboyInAppMessageManager` -> `com.braze.ui.inappmessage.BrazeInAppMessageManager`\n  - Class `com.appboy.ui.inappmessage.DefaultInAppMessageViewWrapper` -> `com.braze.ui.inappmessage.DefaultInAppMessageViewWrapper`\n  - Class `com.appboy.AppboyLifecycleCallbackListener` -> `com.braze.BrazeActivityLifecycleCallbackListener`\n- Changed the `ContentCardsFragment` and `BrazeInAppMessageManager` to clear their respective caches of messages after `wipeData()` is called.\n\n## 14.0.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v14.0.1)\n\n##### Fixed\n- Fixed an issue with `BrazeProperties` not being kept via proguard rules.\n- Fixed an issue on TV integrations where in app messages wouldn't properly be given focus when visible.\n\n##### Added\n- Added close icon highlighting for TV integrations when selecting the close button in In App Messages.\n\n## 14.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v14.0.0)\n\n##### ⚠ Breaking\n- Interface `IInAppMessageViewWrapperFactory` changed to use `BrazeConfigurationProvider`.\n- Interface `IAppboyImageLoader/IBrazeImageLoader` changed to use `com.braze.enums.BrazeViewBounds`.\n- Class `com.appboy.configuration.AppboyConfig` is now `com.braze.configuration.BrazeConfig`. The original class has been removed and old usages should be updated.\n- Class `com.appboy.enums.AppboyViewBounds` is now `com.braze.enums.BrazeViewBounds`. The original class has been removed and old usages should be updated.\n- Removed `com.appboy.push.AppboyNotificationUtils#bundleOptString`.\n- `Braze.logPurchase()` and `Braze.logEvent()` now impose a 50KB limit on event properties. If the supplied properties are too large, the event is not logged.\n  - See `BrazeProperties.isInvalid()`.\n- HTML In-App Messages rendered via the default `AppboyHtmlViewFactory` now require the device to be in touch mode to display.\n  - See `getIsTouchModeRequiredForHtmlInAppMessages()` in the #added section for configuration on disabling this behavior.\n- For Kotlin consumers, `Appboy.currentUser?` calls must be migrated to `Braze.getCurrentUser<BrazeUser>()` due to updated generics resolution.\n\n##### Changed\n- Several classes in the base artifact have been renamed to the Braze namespace/packages. Whenever possible, the original classes are still available. However, they will be removed in a future release so migrating early is highly recommended.\n  - `com.appboy.Appboy` -> `com.braze.Braze`\n  - `com.appboy.configuration.AppboyConfig` -> `com.braze.configuration.BrazeConfig`\n  - `com.braze.AppboyUser` -> `com.braze.BrazeUser`\n  - `com.appboy.lrucache.AppboyLruImageLoader` -> `com.braze.images.DefaultBrazeImageLoader`\n  - `com.appboy.configuration.AppboyConfigurationProvider` -> `com.braze.configuration.BrazeConfigurationProvider`\n  - `com.appboy.models.outgoing.AppboyProperties` -> `com.braze.models.outgoing.BrazeProperties`\n  - `com.appboy.support.AppboyImageUtils` -> `com.braze.support.BrazeImageUtils`\n  - `com.appboy.support.AppboyFileUtils` -> `com.braze.support.BrazeFileUtils`\n- Changed the behavior of In-App Message Accessibility Exclusive mode to save and reset the accessibility flags of views after display.\n- Changed the `AppboyInAppMessageWebViewClientListener` to use an Activity context when following a deeplink in `IInAppMessageWebViewClientListener.onOtherUrlAction`.\n- Deprecated `AppboyInAppMessageHtmlBaseView.APPBOY_BRIDGE_PREFIX`.\n\n##### Added\n- Added `Braze.registerPushToken()` and `Braze.getRegisteredPushToken()`.\n  - Note that these methods are the functional equivalents of `Appboy.registerAppboyPushMessages()` and `Appboy.getAppboyPushMessageRegistrationId()`.\n- Exposed `brazeBridge` which replaces `appboyBridge` to be used as the javascript interface for HTML In-App Messages. `appboyBridge` is deprecated and will be removed in a future version of the SDK.\n- Added `AppboyInAppMessageHtmlBaseView.BRAZE_BRIDGE_PREFIX`.\n- Added the ability to configure whether `View#isInTouchMode()` is required to show HTML In-App Messages via `BrazeConfig.setIsTouchModeRequiredForHtmlInAppMessages()`.\n  - Can also be configured via boolean `com_braze_require_touch_mode_for_html_in_app_messages` in your `braze.xml`.\n  - Defaults to true.\n- Added support for new SDK Authentication feature.\n\n##### Fixed\n- Fixed an issue with `setIsInAppMessageAccessibilityExclusiveModeEnabled()` not being respected if set via runtime configuration. Setting this value via XML was unaffected.\n- Fixed an issue with the SDK repeatedly failing to initialize when not properly setting a Braze API key.\n\n## 13.1.2\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v13.1.2)\n\n##### Changed\n- Changed the `NotificationTrampolineActivity` to always call `finish()` regardless of any eventual deeplink handling by the host app or SDK.\n\n## 13.1.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v13.1.1)\n\n##### Fixed\n- Fixed an issue with the `NotificationTrampolineActivity` being opened on notification delete intents.\n\n## 13.1.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v13.1.0)\n\n##### Changed\n- All notifications now route through `NotificationTrampolineActivity` to comply with Android 12 notification trampoline restrictions.\n- Inline Image push is now compatible with the Android 12 notification area changes.\n- Automatic Firebase Messaging registration will now use `FirebaseMessaging.getInstance().getToken()` directly if available.\n- Removed usage of `Intent.ACTION_CLOSE_SYSTEM_DIALOGS` with push notifications.\n\n##### Added\n- Added `getInAppMessageStack()`, `getCarryoverInAppMessage()`, and `getUnregisteredInAppMessage()` to `AppboyInAppMessageManager`.\n\n## 13.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v13.0.0)\n\n##### ⚠ Breaking\n- Moved all In-App Message buttons from `Button` to `com.appboy.ui.inappmessage.views.InAppMessageButton`.\n  - This ensures that the `MaterialComponentsViewInflater` does not interfere with standard In-App Message display when using a `MaterialComponents` theme.\n  - Apps extending a `Material` theme should test to ensure their In-App Messages appear as expected.\n- Moved `com.appboy.ui.inappmessage.AppboyInAppMessageImageView` to `com.appboy.ui.inappmessage.views.InAppMessageImageView`.\n- Removed all getter methods from `AppboyConfig`. Access to the underlying data is now directly possible via the variables of the object, e.g. `appboyConfig.getApiKey()` is now `appboyConfig.mApiKey`.\n\n##### Added\n- Added `getEmptyCardsAdapter(), getContentCardUpdateRunnable(), getNetworkUnavailableRunnable()` to protected methods in `AppboyContentCardsFragment` for easier customizability.\n- Changed the max content line length to 2 lines for Inline Image Push.\n  - This style can be found via `\"Appboy.Push.InlineImage.TextArea.TitleContent.ContentText\"`\n\n##### Fixed\n- Changed the `AppboyContentCardsFragment.ContentCardsUpdateRunnable` to determine network unavailability and feed emptiness based on the filtered list of cards and not the original input list of cards.\n- Fixed an issue with IAM display where a deleted local image would result in a failed image display.\n\n## 12.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v12.0.0)\n\n##### ⚠ Breaking\n- Added `getIntentFlags` to the `IAppboyNavigator` interface to more easily allow for customizing Activity launch behavior.\n  - A default implementation is available below:\n  ```\n    @Override\n    public int getIntentFlags(IntentFlagPurpose intentFlagPurpose) {\n      return new AppboyNavigator().getIntentFlags(intentFlagPurpose);\n    }\n  ```\n- Renamed `firebase_messaging_service_automatically_register_on_new_token` to `com_appboy_firebase_messaging_service_automatically_register_on_new_token` in `appboy.xml` configuration.\n\n##### Fixed\n- Fixed an issue with the default image loader not properly setting image bitmaps on API 23 and below devices.\n- Fixed an issue where the `AppboyInAppMessageManager.ensureSubscribedToInAppMessageEvents()` method wouldn't properly resubscribe after disabling and re-enabling the SDK.\n\n##### Changed\n- Changed Push Stories in `AppboyNotificationStyleFactory` to use `BrazeNotificationPayload`.\n\n## 11.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v11.0.0)\n\n##### ⚠ Breaking\n- Changed the behavior of new beta HTML In-App Messages with dashboard preview support (i.e. those with `MessageType.HTML` and not `MessageType.HTML_FULL`) to not automatically log analytics clicks on url follows in `IInAppMessageWebViewClientListener`.\n  - Body click analytics will no longer automatically be collected. To continue to receive body click analytics, you must log body clicks explicitly from your message via Javascript using `appboyBridge.logClick()`.\n- `IContentCardsUpdateHandler` and `IContentCardsViewBindingHandler` interfaces now extend `android.os.Parcelable`.\n  - This ensures that these handlers properly transition across instance state saves and reads.\n  - Examples on how to extend `Parcelable` can be found in `DefaultContentCardsUpdateHandler` and `DefaultContentCardsViewBindingHandler`.\n- Renamed `AppboyFcmReceiver` to `BrazePushReceiver`.\n\n##### Added\n- Added `AppboyInAppMessageManager.getIsCurrentlyDisplayingInAppMessage()`.\n- Added ability to configure whether the `AppboyFirebaseMessagingService` will automatically register tokens in its `onNewToken` method.\n  - Defaults to whether FCM automatic registration is enabled. Note that FCM automatic registration is a separate configuration option and is not enabled by default.\n  - Configured by changing the boolean value for `firebase_messaging_service_automatically_register_on_new_token` in your `appboy.xml`, or at runtime by setting `AppboyConfig.setIsFirebaseMessagingServiceOnNewTokenRegistrationEnabled()`.\n  - Note that the Sender ID used to configure tokens received in `onNewToken()` is based on the app's default Firebase Project rather than the explicitly configured Sender ID on the Braze SDK. These should generally be the same value.\n\n##### Changed\n- Deprecated `AppboyLifecycleCallbackListener.setInAppMessagingRegistrationBlacklist()` in favor of `AppboyLifecycleCallbackListener.setInAppMessagingRegistrationBlocklist()`.\n- Deprecated `AppboyConfig.Builder.setDeviceObjectWhitelist()` in favor of `AppboyConfig.Builder.setDeviceObjectAllowlist()`.\n- Deprecated `AppboyConfig.Builder.setDeviceObjectWhitelistEnabled()` in favor of `AppboyConfig.Builder.setDeviceObjectAllowlistEnabled()`.\n\n##### Fixed\n- Fixed an issue where the `AppboyContentCardsFragment` would not transition a custom `IContentCardsUpdateHandler` or `IContentCardsViewBindingHandler` implementation in `onSaveInstanceState()`, which caused the defaults for both to be used instead.\n- Fixed an issue with deeplink handling where push action button deeplinks would only work once throughout the lifetime of the application.\n\n## 10.1.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v10.1.0)\n\n##### Changed\n- Changed `AppboyWebViewActivity` to extend `FragmentActivity` for better fragment management.\n  - Note that `AppboyWebViewActivity` now no longer performs session and in-app message registration on its own.\n  - Clients using `AppboyLifecycleCallbackListener` will see no effect.\n  - Clients performing manual session integration should override `AppboyWebViewActivity` to add back this registration and set the new Activity via `AppboyConfig.Builder#setCustomWebViewActivityClass()` or `com_appboy_custom_html_webview_activity_class_name` in the `appboy.xml` file.\n\n##### Added\n- Added support for receiving messages via the Huawei Messaging Service.\n\n##### Fixed\n- Fixed minor display issues with Inline Image Push.\n\n## 10.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v10.0.0)\n\n##### ⚠ Breaking\n- The Android SDK has now fully migrated to AndroidX dependencies. No backwards compatibility is possible with the no longer maintained Android Support Library.\n  - See https://developer.android.com/jetpack/androidx for more information on AndroidX, including migration steps.\n  - Braze Android 9.0.0 is the last SDK version compatible with the Android Support Library.\n- Added a new interface method, `IAppboyNotificationFactory.createNotification(BrazeNotificationPayload)`.\n  - The `BrazeNotificationPayload` is a data object that performs the task of extracting and surfacing values from the Braze push payload in a far more convenient way.\n  - Integrations without a custom `IAppboyNotificationFactory` will have no breaking changes.\n  - Integrations with a custom `IAppboyNotificationFactory` are recommended to switchover to their non-deprecated counterparts in `AppboyNotificationUtils.java`.\n\n##### Added\n- Added support for `com_appboy_inapp_show_inapp_messages_automatically` boolean configuration for Unity.\n\n##### Fixed\n- Fixed support for dark mode in HTML in-app messages and remote urls opened in `AppboyWebViewActivity` for deeplinks via the `prefers-color-scheme: dark` css style.\n  - The decision to display content in dark mode will still be determined at display time based on the device's state.\n- Fixed an issue where the card parameter in `com.appboy.IAppboyImageLoader.renderUrlIntoCardView()` was null for Content Cards.\n\n##### Removed\n- Removed `com.appboy.push.AppboyNotificationUtils.handleContentCardsSerializedCardIfPresent()`.\n\n## 9.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v9.0.0)\n\n##### ⚠ Breaking\n- The Android SDK now has a source and target build compatibility set to Java 8.\n\n##### Changed\n- Simplified the email regex used in the SDK to centralize most validation on the server.\n  - The original email validation used is reproduced below:\n  ```\n  (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\\\"(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21\\\\x23-\\\\x5b\\\\x5d-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])*\\\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21-\\\\x5a\\\\x53-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])+)\\\\])\n  ```\n\n##### Fixed\n- Fixed an issue where in-app message icon TextViews could throw a `ClassCastException` on certain devices and prevent display.\n\n##### Removed\n- Removed `com.appboy.support.AppboyImageUtils.getBitmap(android.net.Uri)` in favor of `com.appboy.support.AppboyImageUtils.getBitmap(android.content.Context, android.net.Uri, com.appboy.enums.AppboyViewBounds)`.\n- Removed `com.appboy.AppboyAdmReceiver.CAMPAIGN_ID_KEY`.\n  - Use `Constants.APPBOY_PUSH_CAMPAIGN_ID_KEY` instead.\n- Removed `com.appboy.push.AppboyNotificationUtils.isValidNotificationPriority()`.\n\n## 8.1.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v8.1.0)\n\n##### Added support for Android 11 R (API 30).\n- Note that apps targeting API 30 should update to this SDK version.\n\n##### Changed\n- Changed Content Card subscriptions to automatically re-fire when silent push syncs or test send cards are received via push.\n- Improved several accessibility features of In-App Messages and Content Cards as per [Principles for improving app accessibility](https://developer.android.com/guide/topics/ui/accessibility/principles).\n  - Changed non-informative accessibility content descriptions for in-app message and Content Card images to `@null`.\n  - Content Cards now have content descriptions on their views that incorporate the title and description.\n- Changed the `AppboyFirebaseMessagingService` to override the `onNewToken()` method to register a Firebase push token when automatic Firebase registration enabled.\n\n##### Added\n- Added `appboyBridge.getUser().addAlias()` to the javascript interface for HTML In-App Messages.\n- Added `Appboy.getConfiguredApiKey()` to aid in determining if the SDK has an API key properly configured.\n- Added an overload for `IAppboy.getCurrentUser()` that adds an asynchronous callback for when the current user is available instead of blocking on the caller thread.\n  - The following is an example of the full interface:\n  - ```java\n    Appboy.getInstance(mContext).getCurrentUser(new IValueCallback<AppboyUser>() {\n      @Override\n      public void onSuccess(@NonNull AppboyUser currentUser) {\n        currentUser.setFirstName(\"Jared\");\n      }\n\n      @Override\n      public void onError() {}\n    });\n  ```\n  - A convenience class is also provided with `SimpleValueCallback`:\n  - ```java\n    Appboy.getInstance(mContext).getCurrentUser(new SimpleValueCallback<AppboyUser>() {\n      @Override\n      public void onSuccess(@NonNull AppboyUser currentUser) {\n        currentUser.setFirstName(\"Julian\");\n      }\n    });\n  ```\n- Added `AppboyInAppMessageManager.setClickOutsideModalViewDismissInAppMessageView()` allow for the dismissal of a Modal In-App Message when tapping on the frame behind the message itself.\n  - The default (and historical) value is false, meaning that clicks outside the modal do not close the modal.\n  - To toggle the feature on, call: `AppboyInAppMessageManager.getInstance().setClickOutsideModalViewDismissInAppMessageView(true)`\n\n##### Fixed\n- Fixed behavior of the `com.appboy.ui.AppboyContentCardsFragment` to not assign margin of the first card in the feed from the top of the feed.\n- Fixed an issue with Content Card test sends where the test send wouldn't be visible in some conditions.\n- Fixed an issue with regex based event property triggers not working as expected. Previously they had to match the entire string, now they will search for matches as expected. The regex is now also case-insensitive.\n- Fixed an issue with `resolveActivity()` in the default `UriAction` logic not returning a valid `Activity` to handle external deeplinks on Android 11 devices without the `QUERY_ALL_PACKAGES` permission.\n- Fixed an issue introduced in 4.0.1 where upgrading the SDK could result in server configuration values getting removed until the next session start.\n\n##### Removed\n- Removed `AppboyConfig.Builder.setNotificationsEnabledTrackingOn()`.\n- Removed `AppboyImageUtils.getPixelsFromDp()`.\n- Removed `ViewUtils.getDisplayHeight()`.\n\n## 8.0.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v8.0.1)\n\n##### Fixed\n- Fixed an Activity resolution issue in `com.appboy.ui.AppboyWebViewActivity` by removing a call to `setDownloadListener()`.\n- Fixed an implementation issue in 8.0.0 related to setting runtime configuration after stopping the SDK.\n\n## 8.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v8.0.0)\n\n##### ⚠ Breaking\n* Integrators note: most of the changes listed below are on lightly used interfaces that do no affect most clients.\n- Moved `InAppMessageHtmlBase.getAssetsZipRemoteUrl(), InAppMessageHtmlBase.setAssetsZipRemoteUrl()` to `InAppMessageZippedAssetHtmlBase.java`.\n- Moved `AppboyInAppMessageHtmlFullView.APPBOY_BRIDGE_PREFIX` to `AppboyInAppMessageHtmlBaseView.APPBOY_BRIDGE_PREFIX`\n- Renamed `IInAppMessage.getRemoteAssetPathForPrefetch` to `IInAppMessage.getRemoteAssetPathsForPrefetch` and changed signature to List<String>.\n- Renamed `IInAppMessage.setLocalAssetPathForPrefetch` to `IInAppMessage.setLocalAssetPathsForPrefetch` and changed signature to Map<String, String>.\n- Created In-App Message interface `IInAppMessageWithImage` for slideup, modal, and fulls to hold image based methods. These methods have been refactored out of the `IInAppMessage` interface.\n  - These methods are `getImageUrl(), getRemoteImageUrl(), getLocalImageUrl(), getBitmap(), getImageDownloadSuccessful(), setImageUrl(), setLocalImageUrl(), setImageDownloadSuccessful(), setRemoteImageUrl()`, and `setBitmap()`.\n- Content Card backgrounds (in the default UI), now have their colors set via `/android-sdk-ui/src/main/res/drawable-nodpi/com_appboy_content_card_background.xml`.\n- Several Content Cards related style values are now fully decoupled from News Feed values and are enumerated below.\n - The color `@color/com_appboy_card_background_border` is now `@color/com_appboy_content_card_background_border` for Content Cards.\n - The color `@color/com_appboy_card_background_shadow` is now `@color/com_appboy_content_card_background_shadow` for Content Cards.\n - The color `@color/com_appboy_card_background` is now `@color/com_appboy_content_card_background` for Content Cards.\n - The color used for the text in the empty `AppboyContentCardsFragment`, `@color/com_appboy_title` is now `@color/com_appboy_content_card_empty_text_color`.\n- Several News Feed dimensions values also used in Content Card styles now have Content Card specific values, enumerated below. Note that if these values were overridden in your styles for use in Content Cards, they will have to be updated to the new keys.\n - The dimension `@dimen/com_appboy_card_background_border_left` is now `@dimen/com_appboy_content_card_background_border_left`.\n - The dimension `@dimen/com_appboy_card_background_border_right` is now `@dimen/com_appboy_content_card_background_border_right`.\n - The dimension `@dimen/com_appboy_card_background_border_top` is now `@dimen/com_appboy_content_card_background_border_top`.\n - The dimension `@dimen/com_appboy_card_background_border_bottom` is now `@dimen/com_appboy_content_card_background_border_bottom`.\n - The dimension `@dimen/com_appboy_card_background_shadow_bottom` is now `@dimen/com_appboy_content_card_background_shadow_bottom`.\n - The dimension `@dimen/com_appboy_card_background_corner_radius` is now `@dimen/com_appboy_content_card_background_corner_radius`.\n - The dimension `@dimen/com_appboy_card_background_shadow_radius` is now `@dimen/com_appboy_content_card_background_shadow_radius`.\n- Removed `AppboyInAppMessageHtmlJavascriptInterface(Context)` in favor of `AppboyInAppMessageHtmlJavascriptInterface(Context, IInAppMessageHtml)`.\n- Removed `IAppboy.logPushDeliveryEvent()` and `AppboyNotificationUtils.logPushDeliveryEvent()`.\n\n##### Added\n- Added support for upcoming HTML In-App Message templates.\n- Added `appboyBridge.logClick(String), appboyBridge.logClick()` and `appboyBridge.getUser().setLanguage()` to the javascript interface for HTML In-App Messages.\n- Added support for dark mode in HTML in-app messages and remote urls opened in `AppboyWebViewActivity` for deeplinks via the `prefers-color-scheme: dark` css style.\n  - The decision to display content in dark mode will be determined at display time based on the device's state.\n- Added support for dark mode in the default Content Cards UI.\n  - This feature is enabled by default. To disable or change, override the values present in `android-sdk-ui/src/main/res/values-night/colors.xml` and `android-sdk-ui/src/main/res/values-night/dimens.xml`.\n- Added `IAppboy.subscribeToSessionUpdates()` which allows for the host app to be notified when a session is started or ended.\n- Added the ability to optionally set a custom list of location providers when obtaining a single location, such as on session start. See `AppboyConfig.Builder.setCustomLocationProviderNames()` for more information.\n  - The following example showcases instructing the SDK to use `LocationManager.GPS_PROVIDER` and `LocationManager.NETWORK_PROVIDER`.\n    ```\n      new AppboyConfig.Builder()\n          .setCustomLocationProviderNames(EnumSet.of(LocationProviderName.GPS, LocationProviderName.NETWORK));\n    ```\n  - In xml:\n    ```\n      <string-array translatable=\"false\" name=\"com_appboy_custom_location_providers_list\">\n        <item>GPS</item>\n        <item>NETWORK</item>\n      </string-array>\n    ```\n  - By default, only the passive and network providers are used when obtaining a single location from the system.\n  - This change does not affect Braze Geofences.\n\n##### Fixed\n- Fixed an issue where the pending intent flags on a push story only allowed for the main deeplink to be fired once.\n- Fixed behavior of the `com.appboy.ui.AppboyContentCardsFragment` to not double the margin of the first card in the feed from the top of the feed.\n- Fixed an issue where calling `wipeData()` or `disableSdk()` could result in not being able to set runtime configuration afterwards.\n\n##### Changed\n- Deprecated `com.appboy.models.IInAppMessageWithImage#setImageUrl()` in favor of `com.appboy.models.IInAppMessageWithImage#setRemoteImageUrl(String)`.\n\n## 7.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v7.0.0)\n\n##### ⚠ Breaking\n- Made several changes to the default Content Card views to more easily customize and apply ImageView styling.\n  - Changed `Appboy.ContentCards.BannerImage.ImageContainer.Image` to `Appboy.ContentCards.BannerImage.Image`.\n- Removed `com.appboy.ui.contentcards.view.ContentCardViewHolder.createCardImageWithStyle()`.\n\n##### Added\n- Added Czech and Ukrainian language translations for Braze UI elements.\n- Added `android-sdk-base-jetified` and `android-sdk-ui-jetified` to reference jetified SDK AAR artifacts from the artifact repository.\n  - This is a direct replacement for `android-sdk-ui-x` and is a more complete integration path for using the Braze SDK with AndroidX.\n  - Usage as follows:\n  ```\n  dependencies {\n    implementation \"com.appboy:android-sdk-ui-jetified:${BRAZE_SDK_VERSION}\"\n  }\n  ```\n  - If previously using the `android-sdk-ui-x` module, you must replace any imports under the `com.appboy.uix.push` package to be under `com.appboy.ui.push`.\n  - The gradle properties `android.enableJetifier=true` and `android.useAndroidX=true` are no longer required when using androidX libraries with the Braze SDK.\n- Added Material Design Button class names to exported consumer proguard rules.\n  ```\n  -keepnames class android.support.design.button.MaterialButton\n  -keepnames class com.google.android.material.button.MaterialButton\n  ```\n\n##### Fixed\n- Fixed issue in `AppboyCardAdapter` where a card index could be out of bounds when marking a card as seen.\n\n##### Changed\n- In-App Message \"test sends\" from the dashboard now display automatically if your app is in the foreground.\n  - Backgrounded apps will continue to receive a push notification to display the message.\n  - You can disable this feature by changing the boolean value for `com_appboy_in_app_message_push_test_eager_display_enabled` in your `appboy.xml`, or at runtime by setting `AppboyConfig.setInAppMessageTestPushEagerDisplayEnabled()` to false.\n- Changed `UriAction` to be more easily customizable.\n\n##### Removed\n- Removed the `android-sdk-ui-x` module. See the `Added` section for more information.\n- Removed the China Push Sample app.\n\n## 6.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v6.0.0)\n\n##### ⚠ Breaking\n- Slideup and HTML Full In-App Messages now require the device to be in touch mode at the time of display. This is enforced in their respective `IInAppMessageViewFactory` default implementations.\n  - See https://developer.android.com/reference/android/view/View.html#isInTouchMode().\n- Removed `ViewUtils.setFocusableInTouchModeAndRequestFocus()`.\n- `AppboyUnityPlayerNativeActivity`, `AppboyOverlayActivity`, `AppboyUnityNativeInAppMessageManagerListener`, `AppboyUnityPlayerNativeActivity`, `AppboyUnityPlayerNativeActivity`, and `IAppboyUnityInAppMessageListener` have been removed from the `android-sdk-unity` project.\n  - `UnityPlayerNativeActivity` was deprecated in 2015. See https://unity3d.com/unity/beta/unity5.4.0b1.\n\n##### Added\n- Added proper support for navigating and closing Braze In-App Messages with directional-pads/TV remote input devices.\n- Added the ability to customize the in-app message button border radius via `@dimen/com_appboy_in_app_message_button_corner_radius`.\n- Added the ability to customize the in-app message button border color stroke width via `@dimen/com_appboy_in_app_message_button_border_stroke`.\n  - The stroke width used when an in-app message button border is focused is set via `@dimen/com_appboy_in_app_message_button_border_stroke_focused`.\n\n##### Fixed\n- Fixed an issue where Content Cards syncs were suppressed too often.\n- Fixed an issue where in-app messages could not be closed on TVs or other devices without touch interactions.\n\n##### Changed\n- Changed in-app messages to return focus back to the view that previously held focus before a message is displayed as given via `Activity#getCurrentFocus()`.\n\n## 5.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v5.0.0)\n\n##### ⚠ Breaking\n- Added `IInAppMessageView.hasAppliedWindowInsets()`.\n\n##### Added\n- Added `appboyBridge.logClick()` and `appboyBridge.getUser().setLanguage()` to the javascript interface for HTML In-App Messages.\n- Added `Appboy.requestGeofences()` to request a Braze Geofences update for a manually provided GPS coordinate. Automatic Braze Geofence requests must be disabled to properly use this method.\n  - Braze Geofences can only be requested once per session, either automatically by the SDK or manually with the above method.\n- Added the ability to disable Braze Geofences from being requested automatically at session start.\n  - You can do this by configuring the boolean value for `com_appboy_automatic_geofence_requests_enabled` in your `appboy.xml`.\n  - You can also configure this at runtime by setting `AppboyConfig.setAutomaticGeofenceRequestEnabled()`.\n\n##### Fixed\n- Fixed an issue where multiple calls to `ViewCompat.setOnApplyWindowInsetsListener()` could result in in-app messages margins getting applied multiple times instead of exactly once.\n- Fixed an issue where pure white `#ffffffff` in a dark theme in-app message would not be used when the device was in dark mode.\n  - In this case, the original non-dark theme color would be used by the in-app message instead.\n\n## 4.0.2\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v4.0.2)\n\n##### Fixed\n- Fixed an issue introduced in 4.0.0 where Content Card clicks wouldn't get forwarded to the parent RecyclerView based on its View's `clickable` status.\n  - This would result in clicks not being handled or logged for Content Cards.\n\n## 4.0.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v4.0.1)\n\n##### Fixed\n- Fixed an issue where in-app messages could display behind translucent status and navigation bars.\n\n## 4.0.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v4.0.0)\n\n##### Known Issues with version 4.0.0\n- Content Card clicks are not handled or logged for Content Cards due to the `\"Appboy.ContentCards\"` style containing the `\"clickable=true\"` style. This is fixed in SDK version 4.0.2.\n\n##### ⚠ Breaking\n- Added `beforeInAppMessageViewOpened(), afterInAppMessageViewOpened(), beforeInAppMessageViewClosed(), afterInAppMessageViewClosed()` to the `IInAppMessageManagerListener` interface.\n  - These methods are intended to help instrument each stage of the In-App Message View gaining and losing visibility status.\n- Renamed `Card.getIsDismissible()` to `Card.getIsDismissibleByUser()`.\n\n##### Added\n- Added the ability to more easily test In-App Messages from the dashboard when sending a test push by bypassing the need to click the test push notification and instead directly display the test In-App Message when the app is in the foreground.\n  - A push notification will still display if a test In-App Message push is received and the app is in the background.\n  - You can enable this feature by configuring the boolean value for `com_appboy_in_app_message_push_test_eager_display_enabled` in your `appboy.xml`. The default value is false.\n  - You can also enable this feature at runtime by setting `AppboyConfig.setInAppMessageTestPushEagerDisplayEnabled()` to true. The default value is false.\n- Added the ability to customize how In-App Messages views are added to the view hierarchy with a custom `IInAppMessageViewWrapperFactory`.\n  - See `AppboyInAppMessageManager.setCustomInAppMessageViewWrapperFactory()`.\n  - For lightweight customizations, consider extending `DefaultInAppMessageViewWrapper` and overriding `getParentViewGroup()`, `getLayoutParams()`, and `addInAppMessageViewToViewGroup()`.\n  - Addresses https://github.com/Appboy/appboy-android-sdk/issues/138.\n- Added `Card.setIsDismissibleByUser()` to allow for integrators to disable the default swipe-to-dismiss behavior on a per-card basis.\n- Added the ability to set the initial `AppboyLogger` log level via `appboy.xml`.\n  - In your `appboy.xml`, set an integer value for `com_appboy_logger_initial_log_level`. The integer should correspond to a constant in `Log`, such as `Log.VERBOSE` which is 2.\n  - Values set via `AppboyLogger.setLogLevel()` take precedence over values set in `appboy.xml`.\n- Added the ability to use a custom Activity when opening deeplinks inside the app via a WebView. This Activity will be used in place of the default `AppboyWebViewActivity`.\n  - You can do this by configuring the string value for `com_appboy_custom_html_webview_activity_class_name` in your `appboy.xml`. Note that the class name used `appboy.xml` must be the exact class name string as returned from `YourClass.class.getName()`.\n  - You can also configure this at runtime by setting `AppboyConfig.setCustomWebViewActivityClass()`.\n  - To retrieve the url in your custom WebView:\n  ```\n  final Bundle extras = getIntent().getExtras();\n  if (extras.containsKey(Constants.APPBOY_WEBVIEW_URL_EXTRA)) {\n    String url = extras.getString(Constants.APPBOY_WEBVIEW_URL_EXTRA);\n  }\n  ```\n\n##### Fixed\n- Fixed the inability to scroll through Content Cards when not using standard input mechanisms, aiding accessibility.\n  - All Content Card views now have `selectable` and `focusable` attributes set to true.\n  - Amazon Fire TV integrators should update to this version.\n- Changed `AppboyInAppMessageHtmlUserJavascriptInterface.setCustomAttribute()` in the HTML javascript bridge to not coerce `Double` into `Float`.\n- Fixed default Content Card rendering on low screen density devices. Previously, Content Cards could render without a margin and overflow off screen.\n  - `@dimens/com_appboy_content_cards_max_width` now accurately sets the maximum possible width of a Content Card.\n  - `@dimens/com_appboy_content_cards_divider_left_margin` and `@dimens/com_appboy_content_cards_divider_right_margin` are now used to provide a margin for Content Cards when the width of the Content Card does not exceed the max width of `@dimens/com_appboy_content_cards_max_width`.\n- Fixed an issue where images in Content Cards could be resized before they had finished a layout, resulting in an 0 width/height ImageView.\n\n##### Changed\n- `InAppMessageImmersiveBase.getMessageButtons()` is now guaranteed to be non-null. When buttons are not set on the message, this list will be non-null and empty.\n  - Calling `InAppMessageImmersiveBase.setMessageButtons()` with null will instead clear the `MessageButton` list\n- Changed the SDK to compile against the 18.0.0 version of the Firebase Cloud Messaging dependency.\n- Updated the exported `android-sdk-ui` consumer proguard rules to keep javascript interface methods.\n- Changed the WebView used in HTML In-App Messages to have DOM storage enabled via `setDomStorageEnabled(true)`.\n- Changed Content Cards to allow for blank or empty values for the title or description. In these situations, the `TextView`'s visibility is changed to `GONE` in the view hierarchy.\n\n##### Removed\n- Removed `Constants.APPBOY_WEBVIEW_URL_KEY`.\n\n## 3.8.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v3.8.0)\n\n##### ⚠ Breaking\n- Added `renderUrlIntoInAppMessageView()`, `renderUrlIntoCardView()`, `getPushBitmapFromUrl()`, and `getInAppMessageBitmapFromUrl()` to the `IAppboyImageLoader` interface. These methods provide more information about the rendered object. For example, `renderUrlIntoCardView()` provides the `Card` object being rendered in the feed.\n  - `IAppboyImageLoader.renderUrlIntoView()` and `IAppboyImageLoader.getBitmapFromUrl()` have been removed.\n  - For maintaining behavioral parity, `renderUrlIntoInAppMessageView()` and `renderUrlIntoCardView()` can reuse your previous `IAppboyImageLoader.renderUrlIntoView()` implementation while `getPushBitmapFromUrl()` and `getInAppMessageBitmapFromUrl()` can reuse your previous `IAppboyImageLoader.getBitmapFromUrl()` implementation.\n  - The Glide `IAppboyImageLoader` implementation has been updated and can be found [here](https://github.com/Appboy/appboy-android-sdk/blob/master/samples/glide-image-integration/src/main/java/com/appboy/glideimageintegration/GlideAppboyImageLoader.java).\n- Removed `MessageButton#getIsSecondaryButton()` and `MessageButton#setIsSecondaryButton()`.\n\n##### Added\n- Added support for the upcoming feature, In-App Messages in Dark Mode.\n  - Dark Mode enabled messages must be created from the dashboard. Braze does not dynamically theme In-App Messages for Dark Mode.\n  - Added `IInAppMessageThemeable` interface to In-App Messages, which adds `enableDarkTheme()` to In-App Messages.\n  - To configure/disable Braze from automatically applying a Dark Theme (when available from Braze's servers), use a custom `IInAppMessageManagerListener`.\n    - ```\n        if (inAppMessage instanceof IInAppMessageThemeable && ViewUtils.isDeviceInNightMode(AppboyInAppMessageManager.getInstance().getApplicationContext())) {\n          ((IInAppMessageThemeable) inAppMessage).enableDarkTheme();\n        }\n      ```\n- Added `Card.isContentCard()`.\n- Added the ability to use an existing color resource for `com_appboy_default_notification_accent_color` in your `appboy.xml`.\n  - For example: `<color name=\"com_appboy_default_notification_accent_color\">@color/my_color_here</color>`.\n\n##### Fixed\n- Fixed an edge case where the `AppboyInAppMessageManager` could throw an `NullPointerException` if an in-app message was in the process of animating out while `AppboyInAppMessageManager.unregisterInAppMessageManager()` was called.\n- Fixed an issue where multiple subscribers to Content Cards updates could cause a `ConcurrentModificationException` if they simultaneously attempted to mutate the list returned in `ContentCardsUpdatedEvent.getAllCards()`.\n  - `ContentCardsUpdatedEvent.getAllCards()` now returns a shallow copy of the list of Content Cards model objects.\n- Fixed an issue (introduced in 3.7.0) where the background color for fullscreen in-app messages was not set.\n- Fixed an issue (introduced in 3.7.0) were images for fullscreen in-app messages would not appear on API 21 and below devices.\n\n## 3.7.1\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v3.7.1)\n\n##### Added\n- Added `IInAppMessage.setExtras()` to set extras on In-App Messages.\n\n##### Fixed\n- Fixed an issue where a slow loading HTML In-App Message could throw an exception if the Activity changed before `onPageFinished()` was called.\n- Removed `FEATURE_INDETERMINATE_PROGRESS` and `FEATURE_PROGRESS` from `AppboyWebViewActivity`.\n\n## 3.7.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v3.7.0)\n\n##### Known Issues\n- This release introduced issues with in-app message unregistration (`AppboyInAppMessageManager.unregisterInAppMessageManager()`) and fullscreen in-app messages. These issues have been fixed in version 3.8.0 of the SDK.\n\n##### Breaking\n- Added the `applyWindowInsets()` method to `IInAppMessageView` interface. This allows for granular customization at the in-app message view level with respect to device notches.\n- The old configuration key used in `appboy.xml` for disabling location collection `com_appboy_disable_location_collection` is now deleted. This key is replaced by `com_appboy_enable_location_collection`. The default value of `com_appboy_disable_location_collection` is false. Braze location collection is disabled by default starting with Braze SDK version 3.6.0.\n- Removes the Feedback feature from the SDK. All Feedback methods on the SDK, including `Appboy.submitFeedback()` and `Appboy.logFeedbackDisplayed()`, are removed.\n\n##### Fixed\n- Changed the behavior of In-App Messages to allow analytics to be logged again when the same In-App Message is displaying a new time.\n\n##### Changed\n- Improves support for in-app messages on “notched” devices (for example, iPhone X, Pixel 3XL). Full-screen messages now expand to fill the entire screen of any phone, while covering the status bar.\n- Changed the behavior of HTML In-App Messages to not display until the content has finished loading as determined via `WebViewClient#onPageFinished()` on the in-app message's `WebView`.\n\n## 3.6.0\n\n[Release Date](https://github.com/Appboy/appboy-android-sdk/releases/tag/v3.6.0)\n\n##### Breaking\n- External user ids (provided via `Appboy.changeUser()`), are now limited to 997 bytes in UTF-8 encoding.\n  - Existing user IDs will be truncated to 997 bytes in UTF-8 encoding.\n  - New user IDs (via `Appboy.changeUser()`) will be rejected if too long.\n  - This byte limit can be read in code via `Constants#USER_ID_MAX_LENGTH_BYTES`.\n- Added `IInAppMessage.getMessageType()` to return the `MessageType` enum for easier in-app message type checking.\n- Braze location collection is disabled by default. If you choose to use our location services, you must explicitly enable location services.\n  - You can do this by configuring the boolean value for `com_appboy_enable_location_collection` in your `appboy.xml`. The default value is false.\n  - You can also enable location collection at runtime by setting `AppboyConfig.setIsLocationCollectionEnabled()` to true.\n  - The old configuration value `com_appboy_disable_location_collection` in appboy.xml is deprecated. It should be replaced with new configuration value of `com_appboy_enable_location_collection`.\n\n##### Added\n- Added `AppboyContentCardsFragment.getContentCardsRecyclerView()` to obtain the RecyclerView associated with the Content Cards fragment.\n- Added `AppboyInAppMessageManager.getDefaultInAppMessageViewFactory()` to simplify most custom implementations of `IInAppMessageViewFactory`.\n\n##### Changed\n- Changed the click target area of in-app message close buttons to 48dp. The close button drawable was increased to `20dp` from `14dp`.\n  - The width/height in dp of this click target can be configured with a `dimens` override for `com_appboy_in_app_message_close_button_click_area_width` and `com_appboy_in_app_message_close_button_click_area_height` respectively.\n- Changed `UriUtils.getQueryParameters()` to handle the parsing of an opaque/non-hierarchical Uri such as `mailto:` or `tel:`.\n\n## 3.5.0\n\n##### Breaking\n- Removed `IAppboyUnitySupport` interface from Appboy singleton object. Its methods have been added to the `IAppboy` interface.\n- The `IAction` in `IContentCardsActionListener.onContentCardClicked()` is now annotated as `@Nullable`. Previously, this field was always non-null.\n- Fixed an issue where `FLAG_ACTIVITY_NEW_TASK` was not added to configured back stack Activities when opening push. This resulted in push notifications failing to open deep links in that situation.\n  - Custom push back stack Activities are set via `AppboyConfig.setPushDeepLinkBackStackActivityClass()`.\n\n##### Added\n- Added `Appboy.getCachedContentCards()` to provide an easier way to obtain the cached/offline list of Content Cards on the device.\n- Added `Appboy.deserializeContentCard()` to allow for the deserialization of a Content Card. Useful for custom integrations that store the Content Cards data models in their own storage and recreate the Content Card afterwards.\n\n##### Changed\n- Deprecated `Card.isEqualTo()` in favor of using `Card.equals()`.\n\n##### Fixed\n- Fixed behavior in Content Cards and News Feed where cards without a click action wouldn't have their client click listeners called.\n\n## 3.4.0\n\n##### Added\n- Added support for Android 10 Q (API 29).\n  - With the addition of the `android.permission.ACCESS_BACKGROUND_LOCATION` permission in Android Q, this permission is now required for Braze Geofences to work on Android Q+ devices. Please see the documentation for more information.\n  - The `AppboyNotificationRoutingActivity` class is now sent with the `Intent.FLAG_ACTIVITY_NO_HISTORY` Intent flag. This is not expected to be a user visible change nor will require any integration changes.\n- Added the ability to enable Braze Geofences without enabling Braze location collection. Set `AppboyConfig.setGeofencesEnabled()` or `com_appboy_geofences_enabled` in your `appboy.xml` to enable Braze Geofences.\n  - Note that Braze Geofences will continue to work on existing integrations if location collection is enabled and this new configuration is not present. This new configuration is intended for integrations that want Braze Geofences, but not location collection enabled as well.\n- Added `Appboy.setGoogleAdvertisingId()` to pass a Google Advertising ID and Ad Tracking Limiting enabled flag back to Braze. Note that the SDK will not automatically collect either field.\n\n##### Fixed\n- Fixed in-app message buttons not properly respecting colors when using a Material Design style theme.\n\n##### Breaking\n- Geofences on Android Q+ devices will not work without the `android.permission.ACCESS_BACKGROUND_LOCATION` permission.\n- Changed the signature of `IInAppMessageManagerListener.onInAppMessageButtonClicked()` to include the in-app message of the clicked button.\n- Removed the deprecated `AppboyWebViewActivity.URL_EXTRA`. Please use `Constants.APPBOY_WEBVIEW_URL_EXTRA` instead.\n\n## 3.3.0\n\n##### Known Issues\n- If using a defined back stack Activity (set via `AppboyConfig.setPushDeepLinkBackStackActivityClass()`), then push notifications containing deep links won't be opened. This behavior is fixed in 3.4.1.\n\n##### Changed\n- Changed the behavior of push deep links to not restart the launcher activity of the app when clicked.\n- Changed the broadcast receiver responsible for sealing sessions after the session timeout to use `goAsync` to lower the occurrence of ANRs on certain devices.\n  - This ANR would contain the constant `APPBOY_SESSION_SHOULD_SEAL` in the Google Play Console.\n- Changed the default video poster (the large black & white play icon) used by default in HTML in-app messages to be transparent.\n\n##### Added\n- Added support for `long` type event properties.\n\n##### Fixed\n- Fixed fullscreen in-app messages on notched devices rendering with a gap at the top of the in-app message.\n- Fixed behavior of in-app messages where modal display would take up the entire screen after successive rotations on older devices.\n\n## 3.2.2\n\n##### Changed\n- Improved the reliability of the session start location logic when location collection is enabled.\n- Changed the in-app message trigger behavior to not perform custom event triggering until any pending server trigger requests have finished.\n\n##### Fixed\n- Fixed a bug in `AppboyInAppMessageImageView` that made images loaded with Glide appear blurry or not appear when setting an aspect ratio.\n\n## 3.2.1\n\n##### Added\n- Added `AppboyFirebaseMessagingService.handleBrazeRemoteMessage()` to facilitate forwarding a Firebase `RemoteMessage` from your `FirebaseMessagingService` to the `AppboyFirebaseMessagingService`.\n  - `AppboyFirebaseMessagingService.handleBrazeRemoteMessage()` will return false if the argument `RemoteMessage` did not originate from Braze. In that case, the `AppboyFirebaseMessagingService` will do nothing.\n  - A helper method `AppboyFirebaseMessagingService.isBrazePushNotification()` will also return true if the `RemoteMessage` originated from Braze.\n\n#### Fixed\n- Fixed an issue with `AppboyInAppMessageBoundedLayout` having a custom styleable attribute that collided with a preset Android attribute.\n\n## 3.2.0\n\n##### Important\n- Please note the breaking push changes in release 3.1.1 regarding the `AppboyFirebaseMessagingService` before upgrading to this version.\n\n##### Fixed\n- Fixed an issue where a filename's canonical path was not validated during zip file extraction.\n- Fixed an issue where the SDK setup verification would erroneously always log a warning that the `AppboyFcmReceiver` was registered using the old `com.google.android.c2dm.intent.RECEIVE` intent-filter.\n\n##### Changed\n- Improved the look and feel of in-app messages to adhere to the latest UX and UI best practices. Changes affect font sizes, padding, and responsiveness across all message types. Now supports button border styling.\n\n##### Added\n- Added collection of `ActivityManager.isBackgroundRestricted()` to device collection information.\n\n## 3.1.1\n\n##### Breaking\n- Added `AppboyFirebaseMessagingService` to directly use the Firebase messaging event `com.google.firebase.MESSAGING_EVENT`. This is now the required way to integrate Firebase push with Braze. The `AppboyFcmReceiver` should be removed from your `AndroidManifest` and replaced with the following:\n  - ```\n    <service android:name=\"com.appboy.AppboyFirebaseMessagingService\">\n      <intent-filter>\n        <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n      </intent-filter>\n    </service>\n    ```\n  - Also note that any `c2dm` related permissions should be removed from your manifest as Braze does not require any extra permissions for `AppboyFirebaseMessagingService` to work correctly.\n- Changed signature of `Appboy.logPushNotificationActionClicked()`.\n\n##### Added\n- Added ability to render HTML elements in push notifications via `AppboyConfig.setPushHtmlRenderingEnabled()` and also `com_appboy_push_notification_html_rendering_enabled` in your `appboy.xml`.\n  - This allows the ability to use \"multicolor\" text in your push notifications.\n  - Note that html rendering be used on all push notification text fields when this feature is enabled.\n\n##### Fixed\n- Fixed behavior where the app would be reopened after clicking notification action buttons with a \"close\" button.\n- Fixed behavior where in-app messages would not apply proper margins on devices with notched displays and would appear obscured by the notch.\n- Fixed an issue that caused the enum `DeviceKey` to be unavailable in our public API.\n- Fixed an issue in the `AppboyFcmReceiver` where the \"is uninstall tracking push\" method was looking for the extras bundle before its preprocessing into a bundle. This would result in uninstall tracking push being forwarded to your broadcast receiver as a silent push when it should not.\n- Fixed an issue in the `AppboyLruImageLoader` where very large bitmaps stored in the cache could throw `OutOfMemoryError` when retrieving them from the cache.\n\n##### Changed\n- Changed behavior of the Feed and Content Cards image loader to always resize images to their true source aspect ratio after download.\n\n## 3.1.0\n\n##### Breaking\n- Renamed `AppboyNotificationUtils.wakeScreenIfHasPermission()` to `AppboyNotificationUtils.wakeScreenIfAppropriate()`. Wakelocks can now be configured to not wake the device screen for push notifications.\n  - This can be set via `AppboyConfig.setIsPushWakeScreenForNotificationEnabled()` and also `com_appboy_push_wake_screen_for_notification_enabled` in your `appboy.xml`.\n\n##### Added\n- A drop-in `AppboyContentCardsActivity` class has been added which can be used to display Braze Content Cards.\n- Added an appboyBridge ready event to know precisely when the appboyBridge has finished loading in the context of an HTML in-app message.\n  - Example below:\n    ```javascript\n     <script type=\"text/javascript\">\n       function logMyCustomEvent() {\n         appboyBridge.logCustomEvent('My Custom Event');\n       }\n       window.addEventListener('ab.BridgeReady', logMyCustomEvent, false);\n     </script>\n    ```\n- Added `Constants.TRAFFIC_STATS_THREAD_TAG` to identify the Braze network traffic with the `TrafficStats` API.\n- Added the ability to configure a blacklist of Activity classes to disable automatic session handling and in-app message registration in the AppboyLifecycleCallbackListener. See `AppboyLifecycleCallbackListener.setActivityClassInAppMessagingRegistrationBlacklist()`, `AppboyLifecycleCallbackListener.setActivityClassSessionHandlingBlacklist()`, and constructor `AppboyLifecycleCallbackListener(boolean, boolean, Set<Class>, Set<Class>)`.\n\n##### Changed\n- Deprecated the Feedback feature. This feature is disabled for new accounts, and will be removed in a future SDK release.\n- Changed the deprecated status of the `AppboyNotificationUtils.isUninstallTrackingPush()` method. Note that uninstall tracking notifications will not be forwarded to registered receivers.\n- Improved in-app message triggering logic to fall back to lower priority messages when the Braze server aborts templating (e.g. from a Connected Content abort in the message body, or because the user is no longer in the correct segment for the message)\n\n## 3.0.1\n\n##### Changed\n- Deprecated `Card.isRead()` and `Card.setIsRead()`. Please use `Card.isIndicatorHighlighted()` and `Card.setIndicatorHighlighted()` instead.\n\n##### Added\n- Added `Card.isClicked()`. Clicks made through `Card.logClick()` are now saved locally on the device for Content Cards.\n- Added `AppboyConfig.setIsInAppMessageAccessibilityExclusiveModeEnabled()` which forces accessibility readers to only be able to read currently displaying in-app messages and no other screen contents.\n  - This can also be set via `com_appboy_device_in_app_message_accessibility_exclusive_mode_enabled` in your `appboy.xml`.\n\n## 3.0.0\n\n##### Breaking\n- From `AppboyConfig`, removed `getEnableBackgroundLocationCollection()`, `getLocationUpdateTimeIntervalSeconds()`, and `getLocationUpdateDistance()` and their respective setters in `AppboyConfig.Builder`.\n- Removed `AppboyInAppMessageImmersiveBaseView.getMessageButtonsView()`.\n- Removed the Fresco image library from the SDK. To displaying GIFs, you must use a custom image library. Please see `IAppboy#setAppboyImageLoader(IAppboyImageLoader)`.\n  - We recommend the [Glide Image Library](https://bumptech.github.io/glide/) as a Fresco replacement.\n  - `AppboyConfig.Builder.setFrescoLibraryEnabled()` has been removed.\n  - `AppboyConfigurationProvider.getIsFrescoLibraryUseEnabled()` has been removed.\n\n##### Fixed\n- Fixed a NPE issue with the RecyclerView while saving the instance state in the `AppboyContentCardsFragment`.\n\n##### Added\n- Added the ability to set location custom attributes on the html in-app message javascript interface.\n- Added compatibility with androidX dependencies.\n    - This initial release adds direct compatibility for classes found under the `com.appboy.push` package. These classes are commonly used in conjunction with an `IAppboyNotificationFactory`. To use these compatible classes, add the following gradle import: `implementation 'com.appboy:android-sdk-ui-x:VERSION'` and replace your imports to fall under the `com.appboy.uix.push` package.\n    - The gradle properties `android.enableJetifier=true` and `android.useAndroidX=true` are required when using androidX libraries with the Braze SDK.\n- Added nullability annotation to `Appboy` and `AppboyUser` for better Kotlin interoperability.\n- Added the ability to optionally whitelist keys in the device object. See `AppboyConfig.Builder.setDeviceObjectWhitelistEnabled()` and `AppboyConfig.Builder.setDeviceObjectWhitelist()` for more information.\n  - The following example showcases whitelisting the device object to only include the Android OS version and device locale in the device object.\n    ```\n      new AppboyConfig.Builder()\n          .setDeviceObjectWhitelistEnabled(true)\n          .setDeviceObjectWhitelist(EnumSet.of(DeviceKey.ANDROID_VERSION, DeviceKey.LOCALE));\n    ```\n\n##### Removed\n- Removed the ability to optionally track locations in the background.\n- Removed Cross Promotion cards from the News Feed.\n  - Cross Promotion cards have also been removed as a card model and will thus no longer be returned.\n\n##### Changed\n- Updated the Baidu China Push sample to use the version 2.9 Baidu JNI libraries and version 6.1.1.21 of the Baidu jar.\n\n## 2.7.0\n\n##### Breaking\n- Renamed `AppboyGcmReceiver` to `AppboyFcmReceiver`. This receiver is intended to be used for Firebase integrations and thus the `com.google.android.c2dm.intent.REGISTRATION` intent-filter action in your `AndroidManifest` should be removed.\n- Removed `AppboyConfigurationProvider.isGcmMessagingRegistrationEnabled()`, `AppboyConfigurationProvider.getGcmSenderId()`, `AppboyConfig.Builder.setGcmSenderId()`, and `AppboyConfig.Builder.setGcmMessagingRegistrationEnabled()`.\n\n##### Changed\n- Changed custom event property values validation to allow for empty strings.\n\n## 2.6.0\n\n##### Added\n- Introduced support for the Content Cards feature, which will eventually replace the existing News Feed feature and adds significant capability.\n\n##### Breaking\n- Updated the minimum SDK version from 14 (Ice Cream Sandwich) to 16 (Jelly Bean).\n\n##### Added\n- Added `AppboyUser.setLocationCustomAttribute()` and `AppboyUser.unsetLocationCustomAttribute()`.\n\n## 2.5.1\n\n##### Changed\n- Changed the behavior of push stories to ensure that after the story initially appears in the notification tray, subsequent page traversal clicks don't alert the user again.\n\n##### Added\n- The Braze SDK now automatically records when the user has disabled notifications at the app level.\n  - The `appboy.xml` `com_appboy_notifications_enabled_tracking_on` boolean attribute and `AppboyConfig.Builder.setNotificationsEnabledTrackingOn()` have been deprecated and are no longer used.\n  - This allows users to more effectively opt-out of push and leads to a more accurate push notification reachable audience.\n\n##### Fixed\n- Fixed an issue where, when the lock screen was present, notification action button and push story body clicks would not open the application immediately. Added `AppboyNotificationRoutingActivity` for handling notification action button and push story body clicks. \n- Fixed an issue where, for non fullscreen activities targeting API 27, requesting an orientation on activities would throw an exception.\n\n## 2.5.0\n\n##### Breaking\n- Added `isControl()` to the `IInAppMessage` interface.\n- Added `logDisplayFailure()` to the `IInAppMessage` interface. In-app message display failures may affect campaign statistics so care should be taken when logging display failures.\n- Added the `InAppMessageControl` class to represent control in-app messages. Control in-app messages should not be displayed to users and should only call `logImpression()` at render time.\n  - Requesting in-app message display, even if the stack is non-empty, may potentially lead to no in-app message displaying if the in-app message is a control in-app message.\n- Added `AppboyInAppMessageManager.setCustomControlInAppMessageManagerListener()` to modify the lifecycle behavior for control in-app messages.\n- Removed `logInAppMessageClick`, `logInAppMessageButtonClick`, and `logInAppMessageImpression` from Appboy Unity player subclasses and `AppboyUnityActivityWrapper`.\n- Removed `AppboyConfigurationProvider.getIsUilImageCacheDisabled()` and `AppboyConfig.Builder.setDisableUilImageCache()`.\n\n##### Fixed\n- Fixed the issue where in-app messages triggered on session start could potentially be templated with the old user's attributes.\n- Fixed a bug where calling `Appboy.wipeData()` or `Appboy.disableSdk()` could potentially lead to null instances being returned from `Appboy.getInstance()`.\n- Fixed the issue where push deep links did not respect the back stack behavior when instructed to open inside the app's WebView.\n- Fixed a bug where the push received broadcast action contained the host package name twice.\n\n## 2.4.0\n\n##### Fixed\n- Fixed a bug where calling `Appboy.wipeData()` would throw an uncaught exception when the Google Play location services library was not present.\n\n##### Added\n- Added the ability to listen for notification deleted intents from the `AppboyGcmReceiver` via the action suffix `AppboyNotificationUtils.APPBOY_NOTIFICATION_DELETED_SUFFIX`.\n- Added a notification creation timestamp to notifications built from the `AppboyGcmReceiver`. This allows for calculating the duration of a notification. Intents will contain `Constants.APPBOY_PUSH_RECEIVED_TIMESTAMP_MILLIS` in the intent extras bundle.\n\n##### Changed\n- Deprecated `AppboyNotificationUtils.isUninstallTrackingPush()` to always return false. Uninstall tracking no longer requires sending a silent push notification to devices.\n\n## 2.3.0\n\n##### Known Issues with version 2.3.0\n- If the Google Play location services library is not present, calls to `Appboy.wipeData()` will throw an uncaught exception.\n\n##### Breaking\n- Removed the `appboyInAppMessageCustomFontFile` custom xml attribute. Custom font typefaces must now be located in the `res/font` directory.\n  - To override a Braze style, both `android:fontFamily` and `fontFamily` style attributes must be set to maintain compatibility across all SDK versions. Example below:\n  ```\n  <item name=\"android:fontFamily\">@font/YOUR_CUSTOM_FONT_FAMILY</item>\n  <item name=\"fontFamily\">@font/YOUR_CUSTOM_FONT_FAMILY</item>\n  ```\n  - See https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml.html for more information.\n- Removed `AppboyInAppMessageButtonView.java` and `AppboyInAppMessageTextView.java`.\n- Removed the `AppboyGeofenceService`. Geofence integration no longer requires a manifest registration. Any reference to `AppboyGeofenceService` can safely be removed from your manifest.\n- Renamed `AppboyUnityPlayerNativeActivityWrapper` to `AppboyUnityActivityWrapper`.\n\n##### Fixed\n- Fixed a bug where sessions could be opened and closed with a null activity.\n\n##### Added\n- Added the ability to have the Braze SDK automatically register for Firebase Cloud Messaging.\n  - Enabled via `com_appboy_firebase_cloud_messaging_registration_enabled` boolean attribute in XML or via `AppboyConfig.Builder.setIsFirebaseCloudMessagingRegistrationEnabled()`.\n  - The Firebase Cloud Messaging Sender ID is set via `com_appboy_firebase_cloud_messaging_sender_id` string attribute in XML or via `AppboyConfig.Builder.setFirebaseCloudMessagingSenderIdKey()`.\n  - The Firebase Cloud Messaging dependencies must still be compiled into your project. The Braze SDK does not compile any Firebase Cloud Messaging dependencies as part of this release.\n- Added UnityPlayerActivity support to AppboyUnityActivityWrapper. Previously only UnityPlayerNativeActivity was supported.\n- Added a AppboyUnityPlayerActivity class for the UnityPlayerActivity for both prime31 and non-prime31 integrations.\n\n## 2.2.5\n\n##### Added\n- Added support for wiping all customer data created by the Braze SDK via `Appboy.wipeData()`.\n- Added `Appboy.disableSdk()` to disable the Braze SDK. \n- Added `Appboy.enableSdk()` to re-enable the SDK after a call to `Appboy.disableSdk()`.\n\n##### Changed\n- Changed `AppboyInAppMessageWebViewClientListener` to call `onDismissed()` when `onCloseAction()` gets called for HTML in-app messages.\n\n##### Fixed\n- Fixed an issue where internal thread pool executors could get blocked on a long running task and throw `RejectedExecutionException`.\n\n## 2.2.4\n\n##### Added\n- Added `AppboyConfig.Builder.setIsSessionStartBasedTimeoutEnabled()` which optionally sets the session timeout behavior to be either session-start or session-end based. The default behavior is to be session-end based.\n  - The use of this flag is recommended for long (30 minutes or longer) session timeout values.\n  - This value can also be configured via `appboy.xml` with the boolean `com_appboy_session_start_based_timeout_enabled` set to true.\n\n## 2.2.3\n\n##### Added\n- Added support for any custom image library to work with in-app messages and the news feed, including the [Glide Image Library](https://bumptech.github.io/glide/). \n  - Please see `IAppboy#setAppboyImageLoader(IAppboyImageLoader)` for how to set a custom image library.\n- Added the `Glide Image Integration` sample app, showcasing how to use the Glide Library.\n\n#### Changed\n- Updated the proguard rules for Fresco and Notification Enabled Tracking.\n\n## 2.2.2\n\n##### Added\n- The Braze SDK may now optionally record when the user has disabled notifications at the app level.\n  - Enabled via `appboy.xml` using the `com_appboy_notifications_enabled_tracking_on` boolean attribute or via `AppboyConfig.Builder.setNotificationsEnabledTrackingOn()`.\n  - If using proguard in your app and Braze SDK v2.2.2 or below, please add `-keep class android.support.v4.app.NotificationManagerCompat { *; }` to your proguard rules.\n  - (Update) Note that starting with Braze Android SDK Version [`2.5.1`](https://github.com/Appboy/appboy-android-sdk/blob/master/CHANGELOG.md#251), this feature is now automatically enabled.\n\n## 2.2.1\n\n##### Added\n- Added `Other`, `Unknown`, `Not Applicable`, and `Prefer not to Say` options for user gender.\n\n## 2.2.0\n\n##### Breaking\n- Removed `Appboy.requestInAppMessageRefresh()` and removed support for Original in-app messages. Note that all customers on version 2.2.0 and newer should use triggered in-app messages.\n- Changed the signature of most methods on the `IAppboy` interface. Methods that logged values now return void instead of boolean. \n  - `IAppboy.openSession()` now returns void.   \n  - `IAppboy.closeSession` now returns void.\n  - `IAppboy.changeUser()` now returns void. To get the current user, please call `IAppboy.getCurrentUser()`.\n  - `IAppboy.logCustomEvent()` and all method overloads now return void.\n  - `IAppboy.logPurchase()` and all method overloads now return void.\n  - `IAppboy.submitFeedback()` now returns void.\n  - `IAppboy.logPushNotificationOpened()` now returns void.\n  - `IAppboy.logPushNotificationActionClicked()` now returns void.\n  - `IAppboy.logFeedDisplayed()` now returns void.\n  - `IAppboy.logFeedbackDisplayed()` now returns void.\n- Removed `AppboyFeedbackFragment.FeedbackResult.ERROR`.\n- Changed `AppboyFeedbackFragment.FeedbackFinishedListener` to `AppboyFeedbackFragment.IFeedbackFinishedListener`.\n- Changed `AppboyFeedbackFragment.FeedbackResult.SENT` to `AppboyFeedbackFragment.FeedbackResult.SUBMITTED`. \n- Removed `Appboy.fetchAndRenderImage()`. Please use `getAppboyImageLoader().renderUrlIntoView()` instead.\n- Removed `AppboyFileUtils.getExternalStorage()`.\n\n##### Added\n- Added Push Stories, a new push type that uses `DecoratedCustomViewStyle` to display multiple images in a single notification. We recommend posting push stories to a notification channel with vibration disabled to avoid repeated vibrations as the user navigates through the story.\n\n##### Changed\n- The Braze singleton now internally performs most actions on a background thread, giving a very substantial performance boost to all actions on the `Appboy` singleton.\n\n#### Fixed\n- Reduced the number of connections made when the Braze SDK downloads files and images. Note that the amount of data downloaded has not changed.\n\n## 2.1.4\n\n##### Added\n- Added a check on Braze initialization for the \"Calypso AppCrawler\" indexing bot that disables all Braze network requests when found. This prevents erroneous Braze data from being sent for Firebase app indexing crawlers.\n- Added the ability to disable adding an activity to the back stack when automatically following push deep links. Previously, the app's main activity would automatically be added to the back stack.\n  - Enabled via `appboy.xml` using the `com_appboy_push_deep_link_back_stack_activity_enabled` boolean attribute or via `AppboyConfig.Builder.setPushDeepLinkBackStackActivityEnabled()`.\n- Added the ability to specify a custom activity to open on the back stack when automatically following push deep links. Previously, only the app's main activity could be used.\n  - The custom activity is set via `appboy.xml` using the `com_appboy_push_deep_link_back_stack_activity_class_name` string attribute or via `AppboyConfig.Builder.setPushDeepLinkBackStackActivityClass()`. Note that the class name used in the `appboy.xml` must be the exact class name string as returned from `YourClass.class.getName()`.\n- Added the `setLanguage()` method to `AppboyUser` to allow explicit control over the language you use in the Braze dashboard to localize your messaging content.\n\n##### Changed\n- Added support for acquiring wake locks on Android O using the notification channel importance instead of the individual notification's priority.\n\n## 2.1.3\n\n##### Fixed\n- Fixed a bug where implicit intents for custom push broadcast receivers would be suppressed in devices running Android O.\n- Updated the Braze ProGuard configuration to ensure Google Play Services classes required by Geofencing aren't renamed.\n\n## 2.1.2\n\n##### Fixed\n- Fixed a bug where sealed session flushes would not be sent on apps with long session timeouts due to Android O background service limitations.\n\n## 2.1.1\n\n##### Added\n- Added the ability to set a custom API endpoint via `appboy.xml` using the `com_appboy_custom_endpoint` string attribute or via `AppboyConfig.Builder.setCustomEndpoint()`.\n\n##### Fixed\n- Fixed a bug where date custom attributes were formatted in the device's locale, which could result in incorrectly formatted dates. Date custom attributes are now always formatted in `Locale.US`.\n\n## 2.1.0\n\n##### Breaking\n- Updated the minimum SDK version from 9 (Gingerbread) to 14 (Ice Cream Sandwich). \n  - We recommend that session tracking and in-app messages registration be done via an `AppboyLifecycleCallbackListener` instance using [`Application.registerActivityLifecycleCallbacks()`](https://developer.android.com/reference/android/app/Application.html#registerActivityLifecycleCallbacks(android.app.Application.ActivityLifecycleCallbacks)).\n- Removed the deprecated field: `AppboyLogger.LogLevel`. Please use `AppboyLogger.setLogLevel()` and `AppboyLogger.getLogLevel()` instead.\n- Updated the v4 support library dependency to version 26.0.0. To download Android Support Libraries versions 26.0.0 and above, you must add the following line to your top-level `build.gradle` repositories block:\n  ```\n  maven {\n    url \"https://maven.google.com\"\n  }\n  ```\n\n##### Added\n- Added support for Android O notification channels. In the case that a Braze notification does not contain the id for a notification channel, Braze will fallback to a default notification channel. Other than the default notification channel, Braze will not create any channels. All other channels must be programatically defined by the host app.\n  - Note that default notification channel creation will occur even if your app does not target Android O. If you would like to avoid default channel creation until your app targets Android O, do not upgrade to this version.\n  - To set the user facing name of the default Braze notification channel, please use `AppboyConfig.setDefaultNotificationChannelName()`.\n  - To set the user facing description of the default Braze notification channel, please use `AppboyConfig.setDefaultNotificationChannelDescription()`.\n\n##### Changed\n- Updated the target SDK version to 26.\n\n## 2.0.5\n\n##### Fixed\n- Fixed a bug where relative links in `href` tags in HTML in-app messages would get passed as file Uris to the `AppboyNavigator`.\n\n##### Added\n- Added `Double` as a valid value type on `AppboyUser.setCustomUserAttribute()`.\n- Added user aliasing capability. Aliases can be used in the API and dashboard to identify users in addition to their ID.  See the `addAlias` method on [`AppboyUser`](https://appboy.github.io/appboy-android-sdk/javadocs/com/appboy/AppboyUser.html) for more information.\n\n## 2.0.4\n\n##### Changed\n- Made further improvements to Braze singleton initialization performance.\n\n## 2.0.3\n\n##### Changed\n- Enabled TLS 1.2 for Braze HTTPS connections running on API 16+ devices. Previously, for devices running on API 16-20, only TLS 1.0 was enabled by default.\n- Improved Braze singleton initialization performance.\n\n## 2.0.2\n\n##### Fixed\n- Fixed a bug where identifying a user while a request was in flight could cause newly written attributes on the old user to be orphaned in local storage.\n\n## 2.0.1\n\n##### Added\n- Added support for displaying Youtube videos inside of HTML in-app messages and the Braze Webview. For HTML in-app messages, this requires hardware acceleration to be enabled in the Activity where the in-app message is being displayed, please see https://developer.android.com/guide/topics/graphics/hardware-accel.html#controlling. Please note that hardware acceleration is only available on API versions 11 and above.\n- Added the ability to access Braze's default notification builder instance from custom `IAppboyNotificationFactory` instances. This simplifies making small changes to Appboy's default notification handling.\n- Improved `AppboyImageUtils.getBitmap()` by adding the ability to sample images using preset view bounds.\n\n## 2.0.0\n\n##### Breaking\n- Removed the following deprecated methods and fields:\n  - Removed the unsupported method `Appboy.logShare()`.\n  - Removed `Appboy.logPurchase(String, int)`.\n  - Removed `Appboy.logFeedCardImpression()` and `Appboy.logFeedCardClick()`. Please use `Card.logClick()` and `Card.logImpression()` instead.\n  - Removed the unsupported method `Appboy.getAppboyResourceEndpoint()`.\n  - Removed `IAppboyEndpointProvider.getResourceEndpoint()`. Please update your interface implementation if applicable.\n  - Removed `Appboy.registerAppboyGcmMessages()`. Please use `Appboy.registerAppboyPushMessages()` instead.\n  - Removed `AppboyInAppMessageBaseView.resetMessageMargins()`. Please use `AppboyInAppMessageBaseView.resetMessageMargins(boolean)` instead.\n  - Removed `com.appboy.unity.AppboyUnityGcmReceiver`. To open Braze push deep links automatically in Unity, set the boolean configuration parameter `com_appboy_inapp_show_inapp_messages_automatically` to true in your `appboy.xml`.\n  - Removed the unsupported method `AppboyUser.setBio()`.\n  - Removed `AppboyUser.setIsSubscribedToEmails()`. Please use `AppboyUser.setEmailNotificationSubscriptionType()` instead.\n  - Removed `Constants.APPBOY_PUSH_CUSTOM_URI_KEY`. Please use `Constants.APPBOY_PUSH_DEEP_LINK_KEY` instead.\n  - Removed `Constants.APPBOY_CANCEL_NOTIFICATION_TAG`.\n  - Removed `com.appboy.ui.actions.ViewAction` and `com.appboy.ui.actions.WebAction`.\n  - Removed `CardCategory.ALL_CATEGORIES`. Please use `CardCategory.getAllCategories()` instead.\n  - Removed `AppboyImageUtils.storePushBitmapInExternalStorage()`.\n  - Removed `AppboyFileUtils.canStoreAssetsLocally()` and `AppboyFileUtils.getApplicationCacheDir()`.\n  - Removed `InAppMessageModal.getModalFrameColor()` and `InAppMessageModal.setModalFrameColor()`. Please use `InAppMessageModal.getFrameColor()` and `InAppMessageModal.setFrameColor()` instead.\n  - Removed `com.appboy.enums.SocialNetwork`.\n  - Removed `AppboyNotificationUtils.getAppboyExtras()`. Please use `AppboyNotificationUtils.getAppboyExtrasWithoutPreprocessing()` instead.\n  - Removed `AppboyNotificationUtils.setLargeIconIfPresentAndSupported(Context, AppboyConfigurationProvider, NotificationCompat.Builder)`. Please use `AppboyNotificationUtils.setLargeIconIfPresentAndSupported(Context, AppboyConfigurationProvider, NotificationCompat.Builder, Bundle)` instead.\n  - Removed `AppboyInAppMessageManager.hideCurrentInAppMessage()`. Please use `AppboyInAppMessageManager.hideCurrentlyDisplayingInAppMessage()` instead.\n- Changed method signatures for `gotoNewsFeed()` and `gotoURI()` in `IAppboyNavigator`. Please update your interface implementation if applicable.\n- Removed `Appboy.unregisterAppboyPushMessages()`. Please use `AppboyUser.setPushNotificationSubscriptionType()` instead.\n- Moved `getAppboyNavigator()` and `setAppboyNavigator()` from `Appboy.java` to `AppboyNavigator.java`.\n- The Braze Baidu China Push integration now uses the Baidu `channelId` as the push token. Please update your push token registration code to pass `channelId` instead of `userId` into `Appboy.registerAppboyPushMessages()`. The China Push sample has been updated.\n- Removed the `wearboy` and `wear-library` modules. Android Wear 1.0 is no longer supported. Please remove `AppboyWearableListenerService` from your `AndroidManifest.xml` if applicable.\n\n##### Added\n- Added a javascript interface to HTML in-app messages\twith ability to\tlog custom events, purchases, user attributes, navigate users, and close the messaage.\n- Added the ability to set a single delegate object to custom handle all Uris opened by Braze across in-app messages, push, and the news feed. Your delegate object should implement the `IAppboyNavigator` interface and be set using `AppboyNavigator.setAppboyNavigator()`.\n  - See https://github.com/Appboy/appboy-android-sdk/blob/master/droidboy/src/main/java/com/appboy/sample/CustomAppboyNavigator.java for an example implementation.\n  - You must also provide instructions for Braze to navigate to your app's (optional) news feed implementation. To use Braze's default handling, call `AppboyNavigator.executeNewsFeedAction(context, uriAction);`.\n  - Note: Previously, `AppboyNavigator` was only used when opening in-app messages.\n\n##### Changed\n- Removed the need to manually add declarations for Braze's news feed and in-app message activities (`AppboyFeedActivity` and `AppboyWebViewActivity`) to the app `AndroidManifest.xml`. If you have these declarations in your manifest, they can be safely removed.\n- Push notifications with web url click actions now open in an in-app webview instead of the external mobile web browser when clicked.\n\n## 1.19.0\n\n##### Added\n- Added support for registering geofences with Google Play Services and messaging on geofence events. Please reach out to success@braze.com for more information about this feature.\n\n##### Removed\n- Support for share type notification action buttons and custom notification action buttons was removed.\n\n##### Changed\n- Push deep links that can be handled by the current app are automatically opened using the current app. Previously, if another app could handle the deep link as well, a chooser dialog would open.\n  - Thanks to [catacom](https://github.com/catacom)\n  - See https://github.com/Appboy/appboy-android-sdk/pull/71\n- `AppboyImageUtils.storePushBitmapInExternalStorage()` has been deprecated.\n\n## 1.18.0\n\n##### Breaking\n- Renamed the `android-sdk-jar` artifact in the `gh-pages` branch to `android-sdk-base` and changed its format from `jar` to `aar`. Most integrations depend on `android-sdk-ui` and won't need to take any action.\n  - Note: If you were compiling `android-sdk-jar` in your `build.gradle`, you must now compile `android-sdk-base`.\n\n##### Added\n- Added the ability to set custom read and unread icons for News Feed cards. To do so, override the `Appboy.Cards.ImageSwitcher` style in your `styles.xml` and add `appboyFeedCustomReadIcon` and `appboyFeedCustomUnReadIcon` drawable attributes.\n- Added a sample app showcasing the FCM + Braze push integration. See `/samples/firebase-push`.\n- Added a sample app for manual session integration. See `/samples/manual-session-integration`.\n\n##### Removed\n- Removed the `-dontoptimize` flag from Braze's UI consumer proguard rules. See https://github.com/Appboy/appboy-android-sdk/blob/master/android-sdk-ui/appboy-proguard-rules.pro for the latest Proguard config.\n  - Thanks to [mnonnenmacher](https://github.com/mnonnenmacher)\n  - See https://github.com/Appboy/appboy-android-sdk/pull/69\n\n##### Changed\n- Updated the Droidboy project to use the conventional Android Build System folder structure.\n\n## 1.17.0\n\n##### Breaking\n- Added the ability to configure Braze completely at runtime using `Appboy.configure()`. Values set at runtime take precedence over their counterparts in `appboy.xml`. A complete example of Braze runtime configuration is available in our Hello Appboy sample app's [application class](https://github.com/Appboy/appboy-android-sdk/blob/master/hello-appboy/src/main/java/com/appboy/helloworld/HelloAppboyApplication.java).\n  - Renamed `com.appboy.configuration.XmlAppConfigurationProvider` to `com.appboy.configuration.AppboyConfigurationProvider`.\n  - `Appboy.configure(String)` changed to `Appboy.configure(Context, AppboyConfig)`.  To maintain parity, replace your current usage with the following equivalent snippit:\n  ```\n  AppboyConfig appboyConfig = new AppboyConfig.Builder()\n          .setApiKey(\"your-api-key\")\n          .build();\n  Appboy.configure(this, appboyConfig);\n  ```\n\n##### Fixed\n- Fixed an issue where in-app messages triggered off of push clicks wouldn't fire because the push click happened before the in-app message configuration was synced to the device.\n\n##### Changed\n- Updated `Appboy.registerAppboyPushMessages()` to flush the subscription to the server immediately.\n- Improved the accessibility-mode behavior of in-app messages.\n\n## 1.16.0\n\n##### Added\n- Added the ability to toggle outbound network requests from the Braze SDK online/offline. See `Appboy.setOutboundNetworkRequestsOffline()` for more details.\n\n##### Fixed\n- Fixed a bug that caused session sealed automatic data flushes to not occur.\n\n##### Removed\n- Removed Braze notification action button icons and icon constants.\n\n## 1.15.3\n\n##### Fixed\n- Fixed a bug where in-app messages triggered while no activity was registered with `AppboyInAppMessageManager` would be dropped.\n\n## 1.15.2\n\n##### Fixed\n- Fixed a bug where in-app messages triggered while no activity was registered with `AppboyInAppMessageManager` would be displayed without assets.\n\n## 1.15.1\n\n##### Added\n- Added Hebrew localization strings.\n\n##### Changed\n- Improved the initialization time of the Braze SDK.\n\n##### Removed\n- Removed fetching of the device hardware serial number as part of device metadata collection.\n\n## 1.15.0\n\n##### Breaking\n- Deprecated `AppboyInAppMessageManager.hideCurrentInAppMessage()`. Please use `AppboyInAppMessageManager.hideCurrentlyDisplayingInAppMessage()` instead.\n\n##### Added\n- Added the option to handle session tracking and `InAppMessageManager` registration automatically on apps with a minimum supported SDK of API level 14 or above. This is done by registering an `AppboyLifecycleCallbackListener` instance using [`Application.registerActivityLifecycleCallbacks()`](https://developer.android.com/reference/android/app/Application.html#registerActivityLifecycleCallbacks(android.app.Application.ActivityLifecycleCallbacks)). See the Hello Appboy sample app's [application class](https://github.com/Appboy/appboy-android-sdk/blob/master/hello-appboy/src/main/java/com/appboy/helloworld/HelloAppboyApplication.java) for an example.\n- Added support for upgraded in-app messages including image-only messages, improved image sizing/cropping, text scrolling, text alignment, configurable orientation, and configurable frame color.\n- Added support for in-app messages triggered on custom event properties, purchase properties, and in-app message clicks.\n- Added support for templating event properties within in-app messages.\n- Added the ability to optionally open deep links and the main activity of the app automatically when a user clicks a push notification, eliminating the need to write a custom `BroadcastReceiver` for Braze push. To activate, set the boolean property `com_appboy_handle_push_deep_links_automatically` to `true` in your `appboy.xml`. Note that even when automatic deep link opening is enabled, Braze push opened and received intents will still be sent. To avoid double opening, remove your custom `BroadcastReceiver` or modify it to not open deep links.\n\n## 1.14.1\n\n##### Fixed\n- Fixed a bug where images in short news and cross promotion News Feed cards would appear too small on high resolution devices. This bug did not affect Fresco users.\n\n##### Changed\n- Updated Baidu push service jar from v4.6.2.38 to v5.1.0.48.\n\n## 1.14.0\n\n##### Breaking\n- Renamed `disableAllAppboyNetworkRequests()` to `enableMockAppboyNetworkRequestsAndDropEventsMode()` and fixes a bug where calling `Appboy.changeUser()` would cause a network request even in disabled/mocked mode. Note that `enableMockAppboyNetworkRequestsAndDropEventsMode` should only be used in testing environments.\n\n##### Added\n- Added the ability to log negatively-priced purchases.\n- Added the option to sort News Feed cards based on read/unread status.\n- Added a custom News Feed click delegate. To handle News Feed clicks manually, implement `IFeedClickActionListener` and register an instance using `AppboyFeedManager.getInstance().setFeedCardClickActionListener()`.  This enables use-cases such as selectively using the native browser to open web links.\n\n##### Changed\n- Added the ability to include file separators in User Ids.\n- Changes Braze's default Log Level from VERBOSE to INFO. Previously disabled debug log statements are enabled and available for debugging. To change Braze's Log Level, update the value of `AppboyLogger.LogLevel`, e.g. `AppboyLogger.LogLevel = Log.VERBOSE`.\n\n##### Removed\n- Removed `keep` rules from `consumerProguardFiles` automatic Proguard configuration for potentially improved optimization for client apps. Note that client apps that Proguard Braze code must now store release mapping files for Braze to interpret stack traces. If you would like to continue to `keep` all Braze code, add `-keep class bo.app.** { *; }` and `-keep class com.appboy.** { *; }` to your Proguard configuration.\n  - See https://github.com/Appboy/appboy-android-sdk/issues/54\n- Removed `onRetainInstance()` from the Braze News Feed fragment. As a result, the News Feed may be used in nested fragments.\n\n## 1.13.5\n\n##### Added\n- Defined `com_appboy_card_background` to provide simpler control of news feed card background color.\n- Added a convenience method to `Month` to allow direct instantiation from a month integer.\n\n##### Fixed\n- Fixed a database access race condition in changeUser code.\n  - See https://github.com/Appboy/appboy-android-sdk/issues/52 and https://github.com/Appboy/appboy-android-sdk/issues/39\n\n##### Removed\n- Removed optimizations from the private library's Proguard configuration to allow dexing Braze with Jack and Android Gradle Plugin 2.2.0+.\n\n## 1.13.4\n\n##### Added\n- Added ability to set push and email subscription state from Droidboy.\n\n##### Changed\n- Open sourced Braze's Unity plugin library code.\n\n## 1.13.3\n\n##### Added\n- Added the ability to set the large notification icon from within the GCM payload.\n- Added `consumerProguardFiles` automatic Proguard configuration.\n\n##### Fixed\n- Fixed a bug where triggered HTML in-app messages would not always send button analytics.\n\n##### Changed\n- Updated Baidu push service jar from v4.3.0.4 to v4.6.2.38.\n- Updated to log analytics for in-app messages and in-app message buttons with 'NONE' click actions.\n- Updated the Droidboy sample app to use material design.\n- Updated the Hello Appboy sample app to use Proguard.\n\n## 1.13.2\n\n##### Fixed\n- Fixed bug where passing a `JSONObject` with multiple invalid keys or values to the `AppboyProperties` constructor would cause a `ConcurrentModificationException`.\n\n## 1.13.1\n\n##### Fixed\n- Added handling to a case where certain devices were returning null Resources for GCM BroadcastReceiver onReceive contexts.\n\n## 1.13.0\n\n##### Added\n- Added support for action-based, locally triggered in-app messages. In-app messages are now sent to the device at session start with associated trigger events. The SDK will display in-app messages in near real-time when the trigger event associated with a message occurs. Trigger events can be app opens, push opens, purchases, and custom events.\n\n##### Changed\n- Deprecated the old system of requesting in-app message display, now collectively known as 'original' in-app messaging, where messages were limited to displaying at app start.\n\n##### Removed\n- Removed Iab billing example code from Droidboy.\n\n## 1.12.0\n\n##### Breaking\n- Removed the deprecated method `Appboy.requestSlideupRefresh()`.  Please use `Appboy.requestInAppMessageRefresh()` instead.\n- Removed the deprecated class AppboySlideupManager.  Please use AppboyInAppMessageManager instead.\n\n##### Changed\n- HTML in-app message WebViews now use wide viewport mode and load pages in overview mode.\n- Moved `AppboyImageUtils` to the private library with an updated api.\n- Moved `WebContentUtils` to the private library.\n- Renamed `IInAppMessageHtmlBase` to `InAppMessageHtmlBase`.\n- Method count of the private Braze library has decreased by over 600 since version 1.11.0.\n\n##### Removed\n- Removed the partial duplicate of the private library's StringUtils from the ui project.\n\n## 1.11.2\n\n##### Fixed\n- Fixed bug where large and small icons both rendered at full size in notification remoteviews for Honeycomb/ICS.  Now, if a large icon is available, only the large icon is shown.  Otherwise, the small icon is used.\n- Fixed bug where push open logs were under-reported under certain device conditions.\n\n## 1.11.1\n- Placeholder for Unity release.\n\n## 1.11.0\n\n##### Added\n- Creates Activity based Unity in-app messages (fixing an issue where touches on in-app messages were hitting the game behind the in-app message) and removes redundant Unity permissions.\n- Added a method for setting modal frame color on in-app messages, no longer displays in-app messages on asset download failure and adds robustness.\n- Added deep link support to `AppboyUnityGcmReceiver`.\n\n##### Changed\n- Makes the WebView background for HTML in-app messages transparent.  Ensure your HTML in-app messages expect a transparent background.\n- Updated Google Play Services from to 7.5.0 to 8.3.0 and Play Services Support from 1.2.0 to 1.3.0.\n  - See https://github.com/Appboy/appboy-android-sdk/issues/45\n- Updated Braze WebView to support redirects to deep links and enables DOM storage.\n\n## 1.10.3\n\n##### Added\n- Added Android M Support.  Under the runtime permissions model introduced in Android M, location permission must be explicitly obtained from the end user by the integrating app.  Once location permission is granted, Braze will resume location data collection on the subsequent session.\n\n## 1.10.2\n\n##### Added\n- Added the ability to log a custom event from an HTML in-app message. To log a custom event from an HTML in-app message, navigate a user to a url of the form `appboy://customEvent?name=customEventName&p1=v2`, where the `name` URL parameter is the name of the event, and the remaining parameters are logged as String properties on the event.\n\n## 1.10.1\n\n##### Changed\n- Enabled javascript in HTML in-app messages.\n- Deprecated `logShare()` and `setBio()` in the public interface as support in the Braze dashboard has been removed.\n\n## 1.10.0\n\n##### Fixed\n- Fixed an issue where applications in extremely resource starved environments were seeing ANRs from the periodic dispatch `BroadcastReceiver`.  This was not a bug in the Braze code, but a symptom of a failing application.  This updates our periodic dispatch mechanism so it won't have this symptomatic behavior, which in some cases should help developers track down the source of the actual issue (depending on the bug).  Apps that only use the Braze jar file will now have to register `<service android:name=\"com.appboy.services.AppboyDataSyncService\"/>` in their `AndroidManifest.xml` to enable Braze to periodically flush data.\n- Fixed a very rare issue where calling `Context.checkCallingOrSelfPermission()` would cause an exception to be thrown on certain custom Android builds.\n\n##### Changed\n- Updated the News Feed to not show cards in the local cache that have expired.\n\n## 1.9.2\n\n##### Fixed\n- Fixed bug triggered when `AppboyWearableListenerService` was not registered.\n\n## 1.9.0\n\n##### Breaking\n- All users must add the line `-dontwarn com.google.android.gms.**` to their proguard config file if using proguard.\n  - See https://github.com/Appboy/appboy-android-sdk/issues/43\n\n##### Added\n- Added support for analytics from Android Wear devices.\n- Added support for displaying notification action buttons sent from the Braze dashboard.  To allow image sharing on social networks, add the `<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />` permission to your `AndroidManifest.xml`.\n- Added delegate to `FeedbackFinishedListener` enabling modification of feedback messages before they are sent to Appboy.  Also adds a disposition parameter to `onFeedbackFinished()`.\n- Added support for GIF images in the News Feed and in in-app messages via the Facebook Fresco image library (version 0.6.1) as a provided library. If found in the parent app (your app), images and GIFs will be loaded using views from the Fresco library. In order to display GIFs, Fresco must be added as a dependency in the parent app. If not found in the parent app, News Feed cards and in-app messages will not display GIFs. To disable use of the Fresco library in the UI project, set the value of `com_appboy_enable_fresco_library_use` to false (or omit it) in your `appboy.xml`; to enable Fresco use set `com_appboy_enable_fresco_library_use` to true in your `appboy.xml`. ImageView specific attributes for News Feed cards and in-app messages, such as `scaleType`, must now be applied programmatically instead of being applied from `styles.xml`. If using Fresco and proguarding your app, please include http://frescolib.org/docs/proguard.html with your proguard config. If you are not using Fresco, add the `dontwarn com.appboy.ui.**` directive. Note: to use Fresco with Braze it must be initialized when your application launches.\n- Added explicit top and bottom padding values for in-app message buttons to improve button rendering on some phones.  See the `Appboy.InAppMessage.Button` style in `styles.xml`.\n- Added HTML in-app message types. HTML in-app messages consist of html along with an included zipped assets file to locally reference images, css, etc. See `CustomHtmlInAppMessageActionListener` in our Droidboy sample app for an example listener for the callbacks on the actions inside the WebView hosting the HTML in-app message.\n- Added a `setAttributionData()` method to AppboyUser that sets an AttributionData object for the user. Use this method with attribution provider SDKs when attribution events are fired.\n\n##### Changed\n- Removed the need for integrating client apps to log push notifications inside their activity code.  **Please remove all calls to `Appboy.logPushNotificationOpened()` from your app as they are now all handled automatically by Braze.  Otherwise, push opens will be incorrectly logged twice.**\n- In-app message views are now found in the `com.appboy.ui.inappmessage.views` package and in-app message listeners are now found in the `com.appboy.ui.inappmessage.listeners` package.\n\n## 1.8.2\n\n##### Added\n- Added the ability to specify custom fonts for in-app message ui elements via the `appboyInAppMessageCustomFontFile` custom xml attribute.\n- Increases the number of supported currency codes from 22 to 171.  All common currency codes are now supported. The full list of supported codes is available at our <a href=\"https://appboy.github.io/appboy-android-sdk/javadocs/com/appboy/IAppboy.html#logPurchase(java.lang.String,%20java.lang.String,%20java.math.BigDecimal,%20int,%20com.appboy.models.outgoing.AppboyProperties)\">Javadoc</a>.\n- Added the method `isUninstallTrackingPush()` to AppboyNotificationUtils to be able to detect background push sent for Braze uninstall tracking.\n\n##### Changed\n- Updated `BigPictureStyle` to show message in expanded view if summary is not present (after 1.7.0 a summary was required in expanded view to have text appear).\n\n## 1.8.1\n- Internal release for Xamarin, adds `AppboyXamarinFormsFeedFragment`.\n\n## 1.8.0\n\n##### Breaking\n- Updated the minimum sdk version from 8 (froyo) to 9 (gingerbread).\n\n##### Added\n- Added an opt-in location service that logs background location events.\n\n##### Fixed\n- Fixed an in-app message lifecycle listener bug where certain lifecycle events could be fired twice.\n\n## 1.7.3\n\n##### Added\n- Added Braze logging configurability by setting the AppboyLogger.LogLevel.  This is intended to be used in development environments and should not be set in a released application as logging statements are essential for debugging.\n  - See https://github.com/Appboy/appboy-android-sdk/issues/38\n- Added `getAppboyPushMessageRegistrationId()` to the Braze interface to enable retrieval of the GCM/ADM/Baidu registration ID Braze has set for the device.\n\n##### Changed\n- Updated our libraries to build against API level 22.\n- Blacklisted custom attributes may no longer be incremented.\n\n## 1.7.2\n\n##### Added\n- Introduced `AppboyNotificationUtils.getAppboyExtrasWithoutPreprocessing()` to parse Braze extras from GCM/ADM intent extras directly rather than requiring Braze extras to be parsed into a Bundle before being passed into `AppboyNotificationUtils.getAppboyExtras()`.\n- Added the ability to send and retrieve extra key-value pairs via a News Feed card.\n- Added the ability to define custom key-value properties on a custom event or purchase.  Property keys are strings and values may be strings, doubles, ints, booleans, or `java.util.Date` objects.\n\n##### Removed\n- Removed `DownloadUtils.java` from `com.appboy.ui.support`.  The `downloadImageBitmap()` function has been moved to `com.appboy.support.AppboyImageUtils`.\n\n## 1.7.1\n\n##### Added\n- Upgrades Droidboy's custom user attributes and purchases capability and refactors the settings page.\n\n##### Removed\n- Removed requirement to manually integrate Font Awesome into the client app's /assets folder for in-app messages with icons.\n\n## 1.7.0\n\n##### Breaking\n- Added summary subtext in `BigView` style notifications.  This is a breaking change in `BigView` style notification display.  Previously the summary text in `BigView` style notifications was set to the bundle/dashboard summary text if it was present, or the alert message otherwise.  Now the bundle/dashboard summary text is used to set the message subtext, which results in the bundle/dashboard summary text being shown in both the collapsed and expanded views.  See our updated push previews for a visualization of this change.\n\n##### Added\n- Added the ability to set a custom `IAppboyNotificationFactory` to customize push using `Appboy.setCustomAppboyNotificationFactory()`.\n- Added the ability to override title and summary in `BigView` push notifications.\n- Added the ability to set a default large icon for push messages by adding the `com_appboy_push_large_notification_icon` drawable resource to your `appboy.xml`.\n- Added support for modal and full screen style in-app messages.  Also adds support for including fontawesome icons and images with in-app messages, changing colors on in-app message UI elements, expanded customization options, and message resizing for tablets.  Please visit our documentation for more information.\n- Added a sample application (China Sample App) which integrates Baidu Cloud Push and Braze for sending push messages through Braze to devices without Google Services installed.\n- Added `AppboyNotificationUtils.logBaiduNotificationClick()`, a utility method for logging push notification opens from push messages sent via Baidu Cloud Push by Braze.\n\n##### Changed\n- Refactors AppboyNotificationUtils into multiple classes in the com.appboy.push package and the AppboyImageUtils class in com.appboy.\n\n## 1.6.2\n\n##### Added\n- Added a major performance upgrade that reduces CPU usage, memory footprint, and network traffic.\n- Added 26 additional languages to localization support for Braze UI elements.\n- Added local blocking for blacklisted custom attributes, events, and purchases.  However, blacklisted attributes may still be incremented (removed in release 1.7.3).\n- Added the ability to set the accent color for notification in Android Lollipop and above.  This can be done by setting the `com_appboy_default_notification_accent_color` integer in your `appboy.xml`.\n- Updated the News Feed to render wider on tablet screens.\n- Added swipe handling for in-app messages on APIs <= 11.\n\n##### Changed\n- Updated our UI library to build against API level 21.\n\n## 1.6.1\n\n##### Fixed\n- Fixed a timezone bug where short names were used for lookup, causing the default timezone (GMT) to be set in cases where the short name was not equal to the time zone Id.\n- Fixed a bug where multiple pending push intents could override each other in the notification center.\n\n## 1.6.0\n\n##### Fixed\n- Fixed News Feed swipe-refresh `CalledFromWrongThreadException`.\n\n##### Changed\n- Updated the android-L preview support from version 1.5.2 to support the public release of Android 5.0.  Updates the v4 support library dependency to version 21.0.0.\n- `android.permission.GET_ACCOUNTS` is no longer required during initial GCM registration for devices running Jelly Bean and higher.  However, use of this permissions is recommended so that pre-Jelly Bean devices can register with GCM.\n- `android.permission.WAKE_LOCK` is no longer required during initial GCM registration.  However, use of this permissions is recommended to allow notifications to wake the screen and engage users when the notification arrives.\n- No longer overwrite messages in the notification center based on collapse key (GCM) or consolidation key (ADM).  Instead, overwrite based on message title and message alert, or, if specified, a custom notification id.\n- Updated Droidboy to use the most recent Google IAB helper classes.\n\n## 1.5.5\n\n##### Added\n- Added support for displaying Kindle notifications with images.\n\n##### Changed\n- Notifications with a minimum priority specified no longer trigger the device wakelock because Android does not display them in the status bar (they appear silently in the drawer).\n\n##### Removed\n- Removed styleable elements from the UI project. This should have no impact on consuming projects.\n\n## 1.5.4\n\n##### Added\n- Incubates a feature to allow for runtime changes to be made to the API key. Please contact android@braze.com if you want to test this feature.\n- Added support for Big View text summaries, allowing summary text to be displayed under the main text in a notification.\n- Added support for custom URIs to open when a notification is clicked.\n- Added support for notification duration control.  When specified, sets an alarm to remove a notification from the notification center after the specified duration.\n- Added support for notification sounds.  Users can specify a notification sound URI to play with the notification.\n- Added support for changing in-app message duration from the client app.  To do this, you can modify the slideup object passed to you in the `onReceive()` delegate using the new setter method `IInAppMessage.setDurationInMilliseconds()`.\n\n##### Changed\n- Updated `AppboyWebViewActivity` to always fill the parent view.  This forces some previously problematic websites to render at the correct size.\n\n## 1.5.3\n\n##### Added\n- Added the ability to turn off Braze's automatic location collection using the `com_appboy_disable_location_collection` boolean in `appboy.xml`.\n- Added the ability to send location tracking events to Braze manually using setLastKnownLocation on the AppboyUser.  This is intended to be used with `com_appboy_disable_location_collection` set to true so that locations are only being recorded from a single source.\n\n## 1.5.2\n\n##### Added\n- Added support for GCM and ADM messages without collapse keys.\n- Added support for GCM and ADM messages with notification priorities.\n- Enabled setting a registration ID without a full push setup; `registerAppboyGcmMessages()` and `registerAppboyPushMessages()` no longer throw null pointer exceptions if Braze isn't correctly configured to display push messages.\n- Enabled `AppboyWebViewActivity` to download items.\n- Added support for apps built targeting android-L. Braze's process for registering push notifications had previously used an implicit service intent which caused a runtime error. Any apps built against android-L will need to upgrade to this version. However, apps with Braze that are/were built against any other versions of Android will run without issue on android-L. Thus, this is not an urgent upgrade unless you're working with android-L.\n\n##### Removed\n- Removed extraneous features from Droidboy so it's more easily digestible as a sample application.\n\n## 1.5.1\n\n##### Removed\n- Removed obfuscation from parameter names on public models.\n\n## 1.5.0\n\n##### Added\n- Added Kindle Fire support and ADM support.\n- Added read/unread visual indicators to newsfeed cards. Use the configuration boolean com_appboy_newsfeed_unread_visual_indicator_on in appboy.xml to enabled the indicators.  Additionally, moved the `logFeedCardImpression()` and `logFeedCardClick()` methods to the card objects themselves.\n- Added support to image loading in CaptionedImage and Banner cards for dynamic resizing after loading the image url; supports any aspect ratio.\n- Added Hello Appboy sample project that shows a minimal use case of the Braze SDK.\n- Added wake lock to `AppboyGcmReceiver` in the UI project. When the `WAKE_LOCK` permission is set, the screen will be turned on when a notification is received.\n\n##### Changed\n- Moved constants from `AppboyGcmReceiver` (ie: `APPBOY_GCM_NOTIFICATION_TITLE_ID`, etc.) into new `AppboyNotificationUtils` class.\n- Restricted productId to 255 characters for `Appboy.logPurchase()`.\n\n## 1.4.3\n\n##### Removed\n- Removed org.json classes from appboy.jar.\n\n## 1.4.2\n\n##### Added\n- Added summary text for push image notifications.\n- Added a new constant, `APPBOY_LOG_TAG_PREFIX`, for logging which includes the sdk version number.\n\n## 1.4.1\n\n##### Added\n- Added automatic tests to verify that the sdk has integrated correctly.\n- Added an optional quantity amount to in-app-purchases.\n\n##### Changed\n- Changed the device identifier from the device persistent `ANDROID_ID` to a non device persistent identifier for compliance with the new Google Play Terms of Service.\n\n##### Removed\n- Removed default max length and ellipsize properties in the `styles.xml`. The old defaults were set to 5 for maxLines for  newsfeed cards and ellipsize 'end'.\n\n## 1.4.0\n\n##### Added\n- Added categories.\n- Added swipe to refresh functionality to the newsfeed. The swipe to refresh colors are configurable in the colors xml file.\n- Added configurable session timeout to the `appboy xml`.\n- Added images to GCM push notifications.\n- Added email and push notification subscription types for a user. Subscription types are explicitly opted in, subscribed, and unsubscribed. The old email boolean subscribe method has been deprecated.\n\n##### Changed\n- The feedback form now displays error popups to the user on invalid fields.\n\n##### Removed\n- Removed click logging on slideups when action is `None`.\n\n## 1.3.4\n\n##### Changed\n- Minor changes to address some Lint issues in the UI project.\n- Updated the open source AppboyGcmReceiver to use references to R.java for resource identifiers. This became possible when we moved AppboyGcmReceiver.java into the android-sdk-ui project (from the base library JAR).\n\n## 1.3.3\n\n##### Fixed\n- Minor bug fix for a crash that occurred in certain conditions where the News Feed cards were replaced with a smaller set of cards.\n\n## 1.3.2\n\n##### Fixed\n- Fixed a few minor style issues to be closer in line with Eclipse's preferences.\n- Fixed a potential synchronization issue with the AppboyListAdapter.\n- Added the ability to set the avatar image URL for your users.\n- Fixed support for protocol URLs and adds an ActivityAction overload that streamlines the use of deep link and web link actions.\n\n##### Changed\n- Minor update to Chinese language translation.\n- Moved com.appboy.AppboyGcmReceiver to the open source android-sdk-ui project. Also moves some of the constants previously available as AppboyGcmReceiver.* to com.appboy.constants.APPBOY_GCM_*. The CAMPAIGN_ID_KEY previously used in our sample app is still available in com.appboy.AppboyGcmReceiver, but if you were using other constants, you'll have to move the references.\n- Removed input validation on custom attribute key names so that you can use foreign characters and spaces to your heart's desire. Just don't go over the max character limit.\n\n## 1.3.1\n\n##### Changed\n- Updated to version 1.9.1 of Android-Universal-Image-Loader.\n- Added Chinese language translations.\n- Minor cleanup to imports.\n\n## 1.3\n\nBraze version 1.3 provides a substantial upgrade to the slideup code and reorganization for better flexibility moving forward, but at the expense of a number of breaking changes. We've detailed the changes in this changelog and hope that you'll love the added power, increased flexibility, and improved UI that the new Braze slideup provides. If you have any trouble with these changes, feel free to reach out to success@braze.com for help, but most migrations to the new code structure should be relatively painless.\n\n##### Breaking\nNew AppboySlideupManager\n- The AppboySlideupManager has moved to `com.appboy.ui.slideups.AppboySlideupManager.java`.\n- An `ISlideupManagerListener` has been provided to allow the developer to control which slideups are displayed, when they are displayed, as well as what action(s) to perform when a slideup is clicked or dismissed.\n  - The slideup `YOUR-APPLICATION-PACKAGE-NAME.intent.APPBOY_SLIDEUP_CLICKED` event has been replaced by the `ISlideupManagerListener.onSlideupClicked(Slideup slideup, SlideupCloser slideupCloser)` method.\n- Added the ability to use a custom `android.view.View` class to display slideups by providing an `ISlideupViewFactory`.\n- Default handling of actions assigned to the slideup from the Braze dashboard.\n- Slideups can be dismissed by swiping away the view to either the left or the right. (Only on devices running Honeycomb Android 3.1 or higher).\n  - Any slideups that are created to be dismissed by a swipe will automatically be converted to auto dismiss slideups on devices that are not running Android 3.1 or higher.\n\nSlideup model\n- A key value `extras` java.util.Map has been added to provide additional data to the slideup. `Extras` can be on defined on a per slideup basis via the dashboard.\n- The `SlideFrom` field defines whether the slideup originates from the top or the bottom of the screen.\n- The `DismissType` property controls whether the slideup will dismiss automatically after a period of time has lapsed, or if it will wait for interaction with the user before disappearing.\n  - The slideup will be dismissed automatically after the number of milliseconds defined by the duration field have elapsed if the slideup's DismissType is set to AUTO_DISMISS.\n- The ClickAction field defines the behavior after the slideup is clicked: display a news feed, redirect to a uri, or nothing but dismissing the slideup. This can be changed by calling any of the following methods: `setClickActionToNewsFeed()`, `setClickActionToUri(Uri uri)`, or `setClickActionToNone()`.\n- The uri field defines the uri string that the slide up will open when the ClickAction is set to URI. To change this value, use the `setClickActionToUri(Uri uri)` method.\n- Convenience methods to track slideup impression and click events have been added to the `com.appboy.models.Slideup` class.\n  - Impression and click tracking methods have been removed from `IAppboy.java`.\n- A static `createSlideup` method has been added to create custom slideups.\n\nIAppboyNavigator\n- A custom `IAppboyNavigator` can be set via `IAppboy.setAppboyNavigator(IAppboyNavigator appboyNavigator)` which can be used to direct your users to your integrated Braze news feed when certain slideups are clicked. This provides a more seamless experience for your users. Alternatively, you can choose not to provide an IAppboyNavigator, but instead register the new `AppboyFeedActivity` class in your `AndroidManifest.xml` which will open a new Braze news feed Activity when certain slideups are clicked.\n\nOther\n- A new base class, `AppboyBaseActivity`, has been added that extends `android.app.Activity` and integrates Braze session and slideup management.\n- A drop-in `AppboyFeedActivity` class has been added which can be used to display the Braze News Feed.\n\n## 1.2.1\n\n##### Fixed\n- Fixed a ProGuard issue.\n\n## 1.2\n\n##### Added\n- Introduced two new card types (Banner card and Captioned Image card).\n- Added support for sending down key/value pairs as part of a GCM message.\n\n##### Fixed\n- Minor bug fixes.\n\n## 1.1\n\n##### Added\n- Added support for reporting purchases in multiple currencies.\n\n##### Fixed\n- Fixed a bug in caching custom events to a SQLite database.\n- Fixed a validation bug when logging custom events.\n\n##### Changed\n- Deprecated `IAppboy.logPurchase(String, int)`.\n\n## 1.0\n- Initial release\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2023 Braze, Inc.\nAll rights reserved.\n\n* Use of source code or binaries contained within Braze’s SDKs is permitted only to enable use of the Braze platform by customers of Braze.\n* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met.\n* Neither the name of Braze nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "This repository has been permanently moved to https://github.com/braze-inc/braze-android-sdk.\n\n-----\n\n\n![Braze Logo](https://github.com/Appboy/appboy-android-sdk/blob/master/braze-logo.png)\n\n# Android SDK\n\nSuccessful marketing automation is essential to the future of your mobile app. Braze helps you engage your users beyond the download. Visit the following links for details and we'll have you up and running in no time!\n\n- [Braze User Guide](https://www.braze.com/docs/user_guide/introduction/ \"Braze User Guide\")\n- [Braze Developer Guide](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/initial_sdk_setup/android_sdk_integration/ \"Braze Developer Guide\")\n- [KDoc](https://appboy.github.io/appboy-android-sdk/kdoc/ \"Braze Android SDK Class Documentation\")\n- [Javadoc(old)](https://appboy.github.io/appboy-android-sdk/javadocs/ \"Braze Android SDK Class Documentation\"). This Javadoc is discontinued. For up-to-date documentation, please visit the Kotlin Doc (KDoc) instead.\n\n## Version Information\n\n- The Braze Android SDK supports Android 4.1+ / API 16+ (Jelly Bean and up).\n- Last Target SDK Version: 33\n- Kotlin version: `org.jetbrains.kotlin:kotlin-stdlib:1.6.0`\n- Last Compiled Firebase Cloud Messaging Version: 23.0.0\n- Braze uses [Font Awesome](https://fortawesome.github.io/Font-Awesome/) 4.3.0 for in-app message icons. Check out the [cheat sheet](http://fortawesome.github.io/Font-Awesome/cheatsheet/) to browse available icons.\n\n## Components\n\n- `android-sdk-base` - the Braze SDK base analytics library.\n- `android-sdk-ui` - the Braze SDK user interface library for in-app messages, push, and the news feed.\n- `android-sdk-location` - the Braze SDK location library for location and geofences.\n- `droidboy` - a sample app demonstrating how to use Braze in-depth.\n- `android-sdk-unity` - a library that enables Braze SDK integrations on Unity.\n- `samples` - a folder containing several sample apps for various integration options.\n\n## Remote repository for gradle\nThe version should match the git version tag, or the most recent version noted in the changelog. An example dependency declaration is:\n\n```\nallprojects {\n  repositories {\n    maven { url \"https://appboy.github.io/appboy-android-sdk/sdk\" }\n    ...\n  }\n}\n```\n\n```\ndependencies {\n  implementation 'com.appboy:android-sdk-ui:24.3.+'\n  implementation 'com.appboy:android-sdk-location:24.3.+'\n  ...\n}\n```\n\n## Installing android-sdk-ui to Your Local Maven Repository\nTo install the UI library as an AAR file to your local maven repository, run the `install` task with\n`./gradlew install`. You can reference it with groupId `com.appboy` and artifactId `android-sdk-ui`. The version should\nmatch the git version tag, or the most recent version noted in the changelog. An example dependency declaration is:\n\n```\nrepositories {\n  mavenLocal()\n  ...\n}\n```\n\n```\ndependencies {\n  implementation 'com.appboy:android-sdk-ui:24.3.+'\n}\n```\n\n## Questions?\n\nIf you have questions, please contact [support@braze.com](mailto:support@braze.com).\n"
  },
  {
    "path": "android-sdk-location/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\ndependencies {\n    api \"com.appboy:android-sdk-base:${BRAZE_SDK_VERSION}\"\n    compileOnly \"androidx.annotation:annotation:${ANDROIDX_ANNOTATIONS_VERSION}\"\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}\"\n    implementation \"com.google.android.gms:play-services-location:${PLAY_SERVICES_LOCATION_VERSION}\"\n    implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:${KOTLIN_COROUTINES_VERSION}\")\n    implementation \"androidx.core:core:${ANDROIDX_CORE_VERSION}\"\n}\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n  }\n\n  kotlinOptions {\n    freeCompilerArgs = ['-Xjvm-default=all', '-Xopt-in=kotlin.RequiresOptIn']\n    jvmTarget = \"1.8\"\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n"
  },
  {
    "path": "android-sdk-location/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.appboy.location\">\n     <application>\n        <receiver\n          android:name=\"com.braze.location.BrazeActionReceiver\"\n          android:exported=\"false\">\n        </receiver>\n    </application>\n</manifest>\n"
  },
  {
    "path": "android-sdk-location/src/main/java/com/braze/location/BrazeActionReceiver.kt",
    "content": "package com.braze.location\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.location.Location\nimport android.location.LocationManager\nimport android.os.Build\nimport androidx.annotation.Keep\nimport androidx.annotation.VisibleForTesting\nimport com.braze.BrazeInternal\nimport com.braze.Constants\nimport com.braze.enums.GeofenceTransitionType\nimport com.braze.models.outgoing.BrazeLocation\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.google.android.gms.location.Geofence\nimport com.google.android.gms.location.GeofencingEvent\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\n\n@Keep\nclass BrazeActionReceiver : BroadcastReceiver() {\n    @OptIn(DelicateCoroutinesApi::class)\n    override fun onReceive(context: Context?, intent: Intent?) {\n        if (intent == null) {\n            brazelog(W) { \"BrazeActionReceiver received null intent. Doing nothing.\" }\n            return\n        } else if (context == null) {\n            brazelog(W) { \"BrazeActionReceiver received null context. Doing nothing.\" }\n            return\n        }\n        val applicationContext = context.applicationContext\n        // This pending result allows us to perform work off the main thread and receive 10 seconds from the system\n        // to finish processing. By default, a BroadcastReceiver is allowed 5 seconds (due to the ANR limit)\n        // for processing.\n        val pendingResult = goAsync()\n        val actionReceiver = ActionReceiver(applicationContext, intent)\n\n        GlobalScope.launch(Dispatchers.IO) {\n            actionReceiver.run()\n            pendingResult.finish()\n        }\n    }\n\n    @VisibleForTesting\n    internal class ActionReceiver(\n        private val applicationContext: Context,\n        private val intent: Intent\n    ) {\n        private val action: String? = intent.action\n        fun run() {\n            try {\n                performWork()\n            } catch (e: Exception) {\n                // If the action receiver encounters an error, we still have to mark the broadcast receiver's work as finished.\n                brazelog(E, e) {\n                    \"Caught exception while performing the BrazeActionReceiver work. Action: $action Intent: $intent\"\n                }\n            }\n        }\n\n        /**\n         * Performs the work as specified by the intent action.\n         *\n         * @return True iff the work was perform successfully.\n         */\n        @VisibleForTesting\n        fun performWork() {\n            brazelog { \"Received intent with action $action\" }\n            when (action) {\n                null -> {\n                    brazelog { \"Received intent with null action. Doing nothing.\" }\n                }\n                Constants.BRAZE_ACTION_RECEIVER_GEOFENCE_UPDATE_INTENT_ACTION -> {\n                    brazelog { \"BrazeActionReceiver received intent with geofence transition: $action\" }\n                    GeofencingEvent.fromIntent(intent)?.let { handleGeofenceEvent(applicationContext, it) }\n                }\n                Constants.BRAZE_ACTION_RECEIVER_SINGLE_LOCATION_UPDATE_INTENT_ACTION -> {\n                    brazelog { \"BrazeActionReceiver received intent with single location update: $action\" }\n                    val location = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        intent.extras?.getParcelable(LocationManager.KEY_LOCATION_CHANGED, Location::class.java)\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        intent.extras?.get(LocationManager.KEY_LOCATION_CHANGED) as Location?\n                    }\n                    location?.let { handleSingleLocationUpdate(applicationContext, it) }\n                }\n                else -> {\n                    brazelog(W) { \"Unknown intent received in BrazeActionReceiver with action: $action\" }\n                }\n            }\n        }\n\n        companion object {\n            private fun handleSingleLocationUpdate(applicationContext: Context, location: Location): Boolean {\n                try {\n                    BrazeInternal.logLocationRecordedEvent(applicationContext, BrazeLocation(location))\n                } catch (e: Exception) {\n                    brazelog(E, e) { \"Exception while processing single location update\" }\n                    return false\n                }\n                return true\n            }\n\n            /**\n             * Records all geofence transitions in the given geofence event.\n             *\n             * @param applicationContext The application context\n             * @param geofenceEvent Google Play Services geofencing event\n             * @return true if a geofence transition was recorded\n             */\n            @VisibleForTesting\n            fun handleGeofenceEvent(applicationContext: Context, geofenceEvent: GeofencingEvent): Boolean {\n                if (geofenceEvent.hasError()) {\n                    val errorCode = geofenceEvent.errorCode\n                    brazelog(W) { \"Location Services error: $errorCode\" }\n                    return false\n                }\n\n                val transitionType = geofenceEvent.geofenceTransition\n                val triggeringGeofences = geofenceEvent.triggeringGeofences\n                return when {\n                    Geofence.GEOFENCE_TRANSITION_ENTER == transitionType -> {\n                        triggeringGeofences?.forEach { geofence ->\n                            BrazeInternal.recordGeofenceTransition(\n                                applicationContext,\n                                geofence.requestId,\n                                GeofenceTransitionType.ENTER\n                            )\n                        }\n                        true\n                    }\n                    Geofence.GEOFENCE_TRANSITION_EXIT == transitionType -> {\n                        triggeringGeofences?.forEach { geofence ->\n                            BrazeInternal.recordGeofenceTransition(\n                                applicationContext,\n                                geofence.requestId,\n                                GeofenceTransitionType.EXIT\n                            )\n                        }\n                        true\n                    }\n                    else -> {\n                        brazelog(W) { \"Unsupported transition type received: $transitionType\" }\n                        false\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-location/src/main/java/com/braze/location/BrazeInternalGeofenceApi.kt",
    "content": "package com.braze.location\n\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport com.braze.Constants\nimport com.braze.models.BrazeGeofence\nimport com.braze.support.IntentUtils\nimport com.google.android.gms.location.LocationServices\n\n/**\n * An implementation of the geofence calls so that they are contained in a single, external\n * module that clients don't need to include if they don't want location services.\n */\nclass BrazeInternalGeofenceApi : IBrazeGeofenceApi {\n\n    /**\n     * @return the PendingIntent that should be fired when a geofence is triggered.\n     */\n    override fun getGeofenceTransitionPendingIntent(context: Context): PendingIntent {\n        val geofenceIntent = Intent(Constants.BRAZE_ACTION_RECEIVER_GEOFENCE_UPDATE_INTENT_ACTION)\n            .setClass(context, BrazeActionReceiver::class.java)\n        val flags = PendingIntent.FLAG_UPDATE_CURRENT or IntentUtils.getMutablePendingIntentFlags()\n        return PendingIntent.getBroadcast(context, 0, geofenceIntent, flags)\n    }\n\n    /**\n     * Teardown all geofences associated with the given intent.\n     */\n    override fun teardownGeofences(applicationContext: Context, intent: PendingIntent) {\n        LocationServices.getGeofencingClient(applicationContext)\n            .removeGeofences(intent)\n    }\n\n    /**\n     * Register a list of geofences.\n     *\n     * @param context Application context.\n     * @param geofenceList List of [BrazeGeofence] to be registered.\n     * @param geofenceRequestIntent The intent to fire when geofence is triggered.\n     */\n    override fun registerGeofences(\n        context: Context,\n        geofenceList: List<BrazeGeofence>,\n        geofenceRequestIntent: PendingIntent\n    ) {\n        GooglePlayLocationUtils.registerGeofencesWithGooglePlayIfNecessary(context, geofenceList, geofenceRequestIntent)\n    }\n\n    /**\n     * Deletes the geofence cache.\n     */\n    override fun deleteRegisteredGeofenceCache(context: Context) {\n        GooglePlayLocationUtils.deleteRegisteredGeofenceCache(context)\n    }\n}\n"
  },
  {
    "path": "android-sdk-location/src/main/java/com/braze/location/BrazeInternalLocationApi.kt",
    "content": "package com.braze.location\n\nimport android.Manifest\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.location.Location\nimport android.location.LocationManager\nimport android.os.Build\nimport androidx.annotation.VisibleForTesting\nimport com.braze.Constants\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.LocationProviderName\nimport com.braze.models.IBrazeLocation\nimport com.braze.models.outgoing.BrazeLocation\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.IntentUtils\nimport com.braze.support.hasPermission\nimport com.braze.support.nowInMilliseconds\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.asExecutor\nimport java.util.*\n\n/**\n * An implementation of the location calls so that they are contained in a single, external\n * module that clients don't need to include if they don't want location services.\n */\nclass BrazeInternalLocationApi : IBrazeLocationApi {\n    private lateinit var context: Context\n    private lateinit var locationManager: LocationManager\n    private lateinit var appConfigurationProvider: BrazeConfigurationProvider\n    private lateinit var allowedLocationProviders: EnumSet<LocationProviderName>\n\n    private val isLocationCollectionEnabled\n        get() = if (appConfigurationProvider.isLocationCollectionEnabled) {\n            brazelog(I) { \"Location collection enabled via sdk configuration.\" }\n            true\n        } else {\n            brazelog(I) { \"Location collection disabled via sdk configuration.\" }\n            false\n        }\n\n    /**\n     * Initialize the object with some external variables. This function should be called immediately\n     * after the object is created and should be called only once.\n     */\n    override fun initWithContext(\n        context: Context,\n        allowedProviders: EnumSet<LocationProviderName>,\n        appConfigurationProvider: BrazeConfigurationProvider\n    ) {\n        this.context = context\n        this.appConfigurationProvider = appConfigurationProvider\n        this.allowedLocationProviders = allowedProviders\n        locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager\n    }\n\n    /**\n     * Requests a location fix for the session on the device.\n     */\n    @Suppress(\"MissingPermission\", \"ReturnCount\")\n    override fun requestSingleLocationUpdate(manualLocationUpdateCallback: (location: IBrazeLocation) -> Unit): Boolean {\n        if (!isLocationCollectionEnabled) {\n            brazelog(I) { \"Did not request single location update. Location collection is disabled.\" }\n            return false\n        }\n        val hasFinePermission = context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)\n        val hasCoarsePermission = context.hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)\n        if (!(hasCoarsePermission || hasFinePermission)) {\n            // Nothing we can do here without any permissions\n            brazelog(I) { \"Did not request single location update. Neither fine nor coarse location permissions found.\" }\n            return false\n        }\n\n        // Check for a GPS location\n        if (hasFinePermission) {\n            val lastKnownGpsLocationIfValid =\n                getLastKnownGpsLocationIfValid(locationManager)\n            if (lastKnownGpsLocationIfValid != null) {\n                brazelog { \"Setting user location to last known GPS location: $lastKnownGpsLocationIfValid\" }\n                manualLocationUpdateCallback.invoke(BrazeLocation(lastKnownGpsLocationIfValid))\n                return true\n            }\n        }\n        val provider = getSuitableLocationProvider(\n            locationManager,\n            allowedLocationProviders,\n            hasFinePermission,\n            hasCoarsePermission\n        )\n        if (provider == null) {\n            brazelog { \"Could not request single location update. Could not find suitable location provider.\" }\n            return false\n        }\n        brazelog { \"Requesting single location update with provider: $provider\" }\n        return try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n                locationManager.getCurrentLocation(\n                    provider,\n                    null,\n                    Dispatchers.IO.asExecutor()\n                ) { location: Location? ->\n                    brazelog { \"Location manager getCurrentLocation got location: $location\" }\n                    if (location != null) {\n                        manualLocationUpdateCallback.invoke(BrazeLocation(location))\n                    }\n                }\n            } else {\n                requestSingleUpdateFromLocationManager(provider)\n            }\n            true\n        } catch (se: SecurityException) {\n            brazelog(E, se) {\n                \"Failed to request single location update due to security exception from insufficient permissions.\"\n            }\n            false\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to request single location update due to exception.\" }\n            false\n        }\n    }\n\n    /**\n     * Determines the best location provider based on the permissions and list of providers allowed.\n     */\n    @Suppress(\"ComplexCondition\")\n    fun getSuitableLocationProvider(\n        locationManager: LocationManager,\n        allowedProviders: EnumSet<LocationProviderName>,\n        hasFinePermission: Boolean,\n        hasCoarsePermission: Boolean\n    ): String? {\n        var provider: String? = null\n        // Check for our preferred providers in order.\n        // Order set in accordance with https://stackoverflow.com/a/6775456/3745724\n        if (hasFinePermission\n            && allowedProviders.contains(LocationProviderName.GPS)\n            && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)\n        ) {\n            provider = LocationManager.GPS_PROVIDER\n        } else if ((hasCoarsePermission || hasFinePermission)\n            && allowedProviders.contains(LocationProviderName.NETWORK)\n            && locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)\n        ) {\n            provider = LocationManager.NETWORK_PROVIDER\n        } else if (hasFinePermission\n            && allowedProviders.contains(LocationProviderName.PASSIVE)\n            && locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)\n        ) {\n            provider = LocationManager.PASSIVE_PROVIDER\n        }\n        return provider\n    }\n\n    /**\n     * Returns the last known location from the [LocationManager.GPS_PROVIDER], if\n     * not older than [LAST_KNOWN_GPS_LOCATION_MAX_AGE_MS].\n     * Assumes that the [Manifest.permission.ACCESS_FINE_LOCATION] permission is present.\n     */\n    @Suppress(\"MissingPermission\")\n    @VisibleForTesting\n    fun getLastKnownGpsLocationIfValid(locationManager: LocationManager): Location? {\n        if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {\n            return null\n        }\n        val lastKnownGpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)\n            ?: return null\n\n        val ageMs = nowInMilliseconds() - lastKnownGpsLocation.time\n        if (ageMs > LAST_KNOWN_GPS_LOCATION_MAX_AGE_MS) {\n            brazelog(V) { \"Last known GPS location is too old and will not be used. Age ms: $ageMs\" }\n            return null\n        }\n        brazelog { \"Using last known GPS location: $lastKnownGpsLocation\" }\n        return lastKnownGpsLocation\n    }\n\n    @Suppress(\"deprecation\", \"MissingPermission\")\n    private fun requestSingleUpdateFromLocationManager(provider: String) {\n        val locationUpdateIntent =\n            Intent(Constants.BRAZE_ACTION_RECEIVER_SINGLE_LOCATION_UPDATE_INTENT_ACTION)\n                .setClass(context, BrazeActionReceiver::class.java)\n        val flags = PendingIntent.FLAG_UPDATE_CURRENT or IntentUtils.getMutablePendingIntentFlags()\n        val locationUpdatePendingIntent =\n            PendingIntent.getBroadcast(context, 0, locationUpdateIntent, flags)\n        // Using this deprecated method because new method only supports API 30+\n        locationManager.requestSingleUpdate(provider, locationUpdatePendingIntent)\n    }\n\n    companion object {\n        /**\n         * The oldest a location from [LocationManager.getLastKnownLocation] using [LocationManager.GPS_PROVIDER]\n         * can be used before requesting a new fix another provider.\n         */\n        const val LAST_KNOWN_GPS_LOCATION_MAX_AGE_MS = 10 * 60 * 1000 // 10 Minutes\n    }\n}\n"
  },
  {
    "path": "android-sdk-location/src/main/java/com/braze/location/GooglePlayLocationUtils.kt",
    "content": "package com.braze.location\n\nimport android.annotation.SuppressLint\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.SharedPreferences\nimport com.braze.managers.BrazeGeofenceManager\nimport com.braze.managers.IBrazeGeofenceLocationUpdateListener\nimport com.braze.models.BrazeGeofence\nimport com.braze.models.outgoing.BrazeLocation\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.google.android.gms.common.api.ApiException\nimport com.google.android.gms.location.Geofence\nimport com.google.android.gms.location.GeofenceStatusCodes\nimport com.google.android.gms.location.GeofencingRequest\nimport com.google.android.gms.location.LocationServices\nimport com.google.android.gms.location.Priority\n\n@SuppressLint(\"MissingPermission\")\nobject GooglePlayLocationUtils {\n    private const val REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION = \"com.appboy.support.geofences\"\n\n    /**\n     * Requests to register the given list of geofences with Google Play Location Services.\n     *\n     * If a given geofence is already registered with Google Play Location Services, it will not be\n     * needlessly re-registered. Geofences that are registered with Google Play Location Services but\n     * not included in the given list of geofences will be un-registered.\n     *\n     * If the given geofence list is empty, geofences will be un-registered and deleted from local\n     * storage.\n     * @param context used by shared preferences\n     * @param geofenceList list of [BrazeGeofence] objects\n     * @param geofenceRequestIntent pending intent to fire when geofences transition events occur\n     */\n    @JvmStatic\n    fun registerGeofencesWithGooglePlayIfNecessary(\n        context: Context,\n        geofenceList: List<BrazeGeofence>,\n        geofenceRequestIntent: PendingIntent\n    ) {\n        try {\n            val prefs = getRegisteredGeofenceSharedPrefs(context)\n            val registeredGeofences = BrazeGeofenceManager.retrieveBrazeGeofencesFromLocalStorage(prefs)\n\n            val newGeofencesToRegister = geofenceList.filter { newGeofence ->\n                registeredGeofences.none { registeredGeofence ->\n                    registeredGeofence.id == newGeofence.id && registeredGeofence.equivalentServerData(newGeofence)\n                }\n            }\n            val obsoleteGeofenceIds = registeredGeofences.filter { registeredGeofence ->\n                newGeofencesToRegister.none { newGeofence ->\n                    newGeofence.id == registeredGeofence.id\n                }\n            }.map { it.id }\n\n            if (obsoleteGeofenceIds.isNotEmpty()) {\n                brazelog { \"Un-registering ${obsoleteGeofenceIds.size} obsolete geofences from Google Play Services.\" }\n                removeGeofencesRegisteredWithGeofencingClient(context, obsoleteGeofenceIds)\n            } else {\n                brazelog { \"No obsolete geofences need to be unregistered from Google Play Services.\" }\n            }\n            if (newGeofencesToRegister.isNotEmpty()) {\n                brazelog { \"Registering ${newGeofencesToRegister.size} new geofences with Google Play Services.\" }\n                registerGeofencesWithGeofencingClient(context, newGeofencesToRegister, geofenceRequestIntent)\n            } else {\n                brazelog { \"No new geofences need to be registered with Google Play Services.\" }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Exception while adding geofences.\" }\n        }\n    }\n\n    /**\n     * Requests a single location update from Google Play Location Services for the given pending intent.\n     *\n     * @param resultListener A callback of type [IBrazeGeofenceLocationUpdateListener]\n     * which will be informed of the result of location update.\n     */\n    @JvmStatic\n    fun requestSingleLocationUpdateFromGooglePlay(\n        context: Context,\n        resultListener: IBrazeGeofenceLocationUpdateListener\n    ) {\n        try {\n            brazelog { \"Requesting single location update from Google Play Services.\" }\n            LocationServices.getFusedLocationProviderClient(context)\n                .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)\n                .addOnSuccessListener {\n                    brazelog(V) { \"Single location request from Google Play services was successful.\" }\n                    resultListener.onLocationRequestComplete(BrazeLocation(it))\n                }\n                .addOnFailureListener { error: Exception? ->\n                    brazelog(E, error) { \"Failed to get single location update from Google Play services.\" }\n                    resultListener.onLocationRequestComplete(null)\n                }\n        } catch (e: Exception) {\n            brazelog(W, e) { \"Failed to request location update due to exception.\" }\n        }\n    }\n\n    /**\n     * Delete the cache of registered geofences. This will cause any geofences passed to\n     * [.registerGeofencesWithGooglePlayIfNecessary]\n     * on the next call to that method to be registered.\n     */\n    @JvmStatic\n    fun deleteRegisteredGeofenceCache(context: Context) {\n        brazelog { \"Deleting registered geofence cache.\" }\n        getRegisteredGeofenceSharedPrefs(context).edit().clear().apply()\n    }\n\n    /**\n     * Registers a list of [Geofence] with a [com.google.android.gms.location.GeofencingClient].\n     *\n     * @param newGeofencesToRegister List of [BrazeGeofence]s to register\n     * @param geofenceRequestIntent A pending intent to fire on completion of adding the [Geofence]s with\n     * the [com.google.android.gms.location.GeofencingClient].\n     */\n    private fun registerGeofencesWithGeofencingClient(\n        context: Context,\n        newGeofencesToRegister: List<BrazeGeofence>,\n        geofenceRequestIntent: PendingIntent\n    ) {\n        val newGooglePlayGeofencesToRegister = newGeofencesToRegister.map { it.toGeofence() }\n        val geofencingRequest = GeofencingRequest.Builder()\n            .addGeofences(newGooglePlayGeofencesToRegister) // no initial trigger\n            .setInitialTrigger(0)\n            .build()\n        LocationServices.getGeofencingClient(context).addGeofences(geofencingRequest, geofenceRequestIntent)\n            .addOnSuccessListener {\n                brazelog { \"Geofences successfully registered with Google Play Services.\" }\n                storeGeofencesToSharedPrefs(context, newGeofencesToRegister)\n            }\n            .addOnFailureListener { geofenceError: Exception? ->\n                if (geofenceError is ApiException) {\n                    when (val statusCode = geofenceError.statusCode) {\n                        GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES -> brazelog(W) {\n                            \"Geofences not registered with Google Play Services due to GEOFENCE_TOO_MANY_GEOFENCES: $statusCode\"\n                        }\n                        GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS -> brazelog(W) {\n                            \"Geofences not registered with Google Play Services due to GEOFENCE_TOO_MANY_PENDING_INTENTS: $statusCode\"\n                        }\n                        GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE -> brazelog(W) {\n                            \"Geofences not registered with Google Play Services due to GEOFENCE_NOT_AVAILABLE: $statusCode\"\n                        }\n                        GeofenceStatusCodes.SUCCESS ->\n                            // Since we're in the failure listener, we don't expect this status code to appear. Nonetheless, it would\n                            // be good to not surface this status code as unknown\n                            brazelog {\n                                \"Received Geofence registration success code in failure block with Google Play Services.\"\n                            }\n                        else -> brazelog(W) { \"Geofence pending result returned unknown status code: $statusCode\" }\n                    }\n                } else {\n                    brazelog(E, geofenceError) { \"Geofence exception encountered while adding geofences.\" }\n                }\n            }\n    }\n\n    /**\n     * Un-registers a list of [Geofence] with a [com.google.android.gms.location.GeofencingClient].\n     *\n     * @param obsoleteGeofenceIds List of [String]s containing Geofence IDs that needs to be un-registered\n     */\n    private fun removeGeofencesRegisteredWithGeofencingClient(context: Context, obsoleteGeofenceIds: List<String>) {\n        LocationServices.getGeofencingClient(context).removeGeofences(obsoleteGeofenceIds)\n            .addOnSuccessListener {\n                brazelog { \"Geofences successfully un-registered with Google Play Services.\" }\n                removeGeofencesFromSharedPrefs(context, obsoleteGeofenceIds)\n            }\n            .addOnFailureListener { geofenceError: Exception? ->\n                if (geofenceError is ApiException) {\n                    when (val statusCode = geofenceError.statusCode) {\n                        GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES -> brazelog(W) {\n                            \"Geofences cannot be un-registered with Google Play Services due to GEOFENCE_TOO_MANY_GEOFENCES: $statusCode\"\n                        }\n                        GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS -> brazelog(W) {\n                            \"Geofences cannot be un-registered with Google Play Services due to GEOFENCE_TOO_MANY_PENDING_INTENTS: $statusCode\"\n                        }\n                        GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE -> brazelog(W) {\n                            \"Geofences cannot be un-registered with Google Play Services due to GEOFENCE_NOT_AVAILABLE: $statusCode\"\n                        }\n                        GeofenceStatusCodes.SUCCESS ->\n                            // Since we're in the failure listener, we don't expect this status code to appear. Nonetheless, it would\n                            // be good to not surface this status code as unknown\n                            brazelog {\n                                \"Received Geofence un-registration success code in failure block with Google Play Services.\"\n                            }\n                        else -> brazelog(W) { \"Geofence pending result returned unknown status code: $statusCode\" }\n                    }\n                } else {\n                    brazelog(E, geofenceError) { \"Geofence exception encountered while removing geofences.\" }\n                }\n            }\n    }\n\n    /**\n     * Returns a [SharedPreferences] instance holding list of registered [BrazeGeofence]s.\n     */\n    private fun getRegisteredGeofenceSharedPrefs(context: Context): SharedPreferences =\n        context.getSharedPreferences(REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION, Context.MODE_PRIVATE)\n\n    /**\n     * Stores the list of [BrazeGeofence] which are successfully registered.\n     *\n     * @param newGeofencesToRegister List of [BrazeGeofence]s to store in SharedPreferences\n     */\n    private fun storeGeofencesToSharedPrefs(context: Context, newGeofencesToRegister: List<BrazeGeofence>) {\n        val editor = getRegisteredGeofenceSharedPrefs(context).edit()\n        for (brazeGeofence in newGeofencesToRegister) {\n            editor.putString(brazeGeofence.id, brazeGeofence.forJsonPut().toString())\n            brazelog(V) { \"Geofence with id: ${brazeGeofence.id} added to shared preferences.\" }\n        }\n        editor.apply()\n    }\n\n    /**\n     * Removes the list of [BrazeGeofence] which are now un-registered with Google Play Services.\n     *\n     * @param obsoleteGeofenceIds List of [String]s containing Geofence IDs that are un-registered\n     */\n    private fun removeGeofencesFromSharedPrefs(context: Context, obsoleteGeofenceIds: List<String>) {\n        val editor = getRegisteredGeofenceSharedPrefs(context).edit()\n        for (id in obsoleteGeofenceIds) {\n            editor.remove(id)\n            brazelog(V) { \"Geofence with id: $id removed from shared preferences.\" }\n        }\n        editor.apply()\n    }\n}\n\n/**\n * Creates a Google Play Location Services Geofence object from a BrazeGeofence.\n * @return A Geofence object.\n */\nfun BrazeGeofence.toGeofence(): Geofence {\n    val builder = Geofence.Builder()\n    builder\n        .setRequestId(id)\n        .setCircularRegion(latitude, longitude, radiusMeter.toFloat())\n        .setNotificationResponsiveness(notificationResponsivenessMs)\n        .setExpirationDuration(Geofence.NEVER_EXPIRE)\n    var transitionTypes = 0\n    if (enterEvents) {\n        transitionTypes = transitionTypes or Geofence.GEOFENCE_TRANSITION_ENTER\n    }\n    if (exitEvents) {\n        transitionTypes = transitionTypes or Geofence.GEOFENCE_TRANSITION_EXIT\n    }\n    builder.setTransitionTypes(transitionTypes)\n    return builder.build()\n}\n"
  },
  {
    "path": "android-sdk-ui/braze-consumer-proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in android-sdk/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n#\n# See https://github.com/Appboy/appboy-android-sdk/issues/49\n-keepnames class com.braze.ui.** { *; }\n-keepnames class com.braze.** { *; }\n-keepnames class bo.app.** { *; }\n\n-dontwarn com.braze.ui.**\n-dontwarn com.google.firebase.messaging.**\n\n-keepclassmembers class * {\n   @android.webkit.JavascriptInterface <methods>;\n}\n"
  },
  {
    "path": "android-sdk-ui/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\ndependencies {\n  api \"com.appboy:android-sdk-base:${BRAZE_SDK_VERSION}\"\n  compileOnly \"com.google.android.material:material:${GOOGLE_MATERIAL_VERSION}\"\n\n  implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:${KOTLIN_COROUTINES_VERSION}\"\n  implementation \"androidx.core:core:${ANDROIDX_CORE_VERSION}\"\n  implementation \"androidx.fragment:fragment:${ANDROIDX_FRAGMENT_VERSION}\"\n  implementation \"androidx.recyclerview:recyclerview:${ANDROIDX_RECYCLERVIEW_VERSION}\"\n  implementation \"androidx.swiperefreshlayout:swiperefreshlayout:${ANDROIDX_SWIPE_REFRESH_LAYOUT_VERSION}\"\n  implementation \"androidx.webkit:webkit:${ANDROIDX_WEBKIT_VERSION}\"\n\n  // Required for the FirebaseMessagingService\n  compileOnly \"com.google.firebase:firebase-messaging:${FIREBASE_PUSH_MESSAGING_VERSION}\"\n}\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    consumerProguardFiles 'braze-consumer-proguard-rules.pro'\n  }\n\n  kotlinOptions {\n    freeCompilerArgs = ['-Xjvm-default=all']\n    jvmTarget = \"1.8\"\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.braze.ui\">\n  <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"/>\n  <application>\n    <activity\n      android:name=\"com.braze.ui.BrazeWebViewActivity\"\n      android:exported=\"false\" />\n    <activity\n      android:name=\".activities.BrazeFeedActivity\"\n      android:exported=\"false\" />\n    <activity\n      android:name=\"com.braze.ui.activities.ContentCardsActivity\"\n      android:exported=\"false\" />\n    <activity\n      android:name=\"com.braze.push.NotificationTrampolineActivity\"\n      android:launchMode=\"singleInstance\"\n      android:exported=\"false\"\n      android:theme=\"@style/Braze.PushTrampoline.Transparent\" />\n    <receiver\n      android:name=\"com.braze.push.BrazePushReceiver\"\n      android:exported=\"false\" />\n  </application>\n</manifest>\n"
  },
  {
    "path": "android-sdk-ui/src/main/assets/braze-html-in-app-message-bridge.js",
    "content": "var brazeBridge = {\n  logCustomEvent: function (name, properties) { brazeInternalBridge.logCustomEventWithJSON(name, JSON.stringify(properties)); },\n  logPurchase: function (productId, price, currencyCode, quantity, purchaseProperties) {\n    brazeInternalBridge.logPurchaseWithJSON(productId, price, currencyCode, quantity != null ? quantity : 1, JSON.stringify(purchaseProperties));\n  },\n  closeMessage: function () { window.location = 'appboy://close'; },\n  requestImmediateDataFlush: function () { brazeInternalBridge.requestImmediateDataFlush(); },\n  display: { showFeed: function () { window.open('appboy://feed'); } },\n  logClick: function(buttonId) {\n    if (buttonId != null) {\n      brazeInternalBridge.logButtonClick(buttonId);\n    } else {\n      brazeInternalBridge.logClick();\n    }\n  },\n  requestPushPermission: function () { brazeInternalBridge.requestPushPermission() },\n  brazeBridgeUserObject: {\n    setFirstName: function(firstName) { brazeInternalBridge.getUser().setFirstName(firstName); },\n    setLastName: function(lastName) { brazeInternalBridge.getUser().setLastName(lastName); },\n    setEmail: function(email) { brazeInternalBridge.getUser().setEmail(email); },\n    setGender: function(gender) { brazeInternalBridge.getUser().setGender(gender); },\n    setHomeCity: function(homeCity) { brazeInternalBridge.getUser().setHomeCity(homeCity); },\n    setEmailNotificationSubscriptionType: function(subscriptionType) {\n      brazeInternalBridge.getUser().setEmailNotificationSubscriptionType(subscriptionType);\n    },\n    setPushNotificationSubscriptionType: function(subscriptionType) {\n      brazeInternalBridge.getUser().setPushNotificationSubscriptionType(subscriptionType);\n    },\n    addToCustomAttributeArray: function(key, value) { brazeInternalBridge.getUser().addToCustomAttributeArray(key, value); },\n    removeFromCustomAttributeArray: function(key, value) { brazeInternalBridge.getUser().removeFromCustomAttributeArray(key, value); },\n    incrementCustomUserAttribute: function(key) { brazeInternalBridge.getUser().incrementCustomUserAttribute(key); },\n    setDateOfBirth: function(year, month, day) { brazeInternalBridge.getUser().setDateOfBirth(year, month, day); },\n    setCountry: function(country) { brazeInternalBridge.getUser().setCountry(country); },\n    setPhoneNumber: function(phone) { brazeInternalBridge.getUser().setPhoneNumber(phone); },\n    setCustomUserAttribute: function(key, value) {\n      var isArray = function(value) {\n          if (Array.isArray) {\n            return Array.isArray(value);\n          }\n          return Object.prototype.toString.call(value) === '[object Array]';\n      };\n      if (isArray(value)) {\n        brazeInternalBridge.getUser().setCustomUserAttributeArray(key, JSON.stringify(value));\n      } else {\n        brazeInternalBridge.getUser().setCustomUserAttributeJSON(key, JSON.stringify({\"value\":value}));\n      }\n    },\n    setLocationCustomUserAttribute: function(key, latitude, longitude) {\n      console.log(\"setLocationCustomUserAttribute() is deprecated and only support on Android. Please use setCustomLocationAttribute() instead.\");\n      brazeInternalBridge.getUser().setCustomLocationAttribute(key, latitude, longitude);\n    },\n    setCustomLocationAttribute: function(key, latitude, longitude) { brazeInternalBridge.getUser().setCustomLocationAttribute(key, latitude, longitude); },\n    setLanguage: function(language) { brazeInternalBridge.getUser().setLanguage(language); },\n    addAlias: function(alias, label) { brazeInternalBridge.getUser().addAlias(alias, label); },\n    addToSubscriptionGroup: function(subscriptionGroupId) { brazeInternalBridge.getUser().addToSubscriptionGroup(subscriptionGroupId); },\n    removeFromSubscriptionGroup: function(subscriptionGroupId) { brazeInternalBridge.getUser().removeFromSubscriptionGroup(subscriptionGroupId); }\n  },\n  getUser: function() {\n    return this.brazeBridgeUserObject;\n  },\n  changeUser: function(userId, sdkAuth = null) {\n      brazeInternalBridge.changeUser(userId, sdkAuth);\n  },\n  web: {\n    registerAppboyPushMessages: function(successCallback, deniedCallback) { console.log(\"This method is a no-op on Android.\"); },\n    trackLocation: function() { console.log(\"This method is a no-op on Android.\"); },\n  }\n};\nvar appboyBridge = brazeBridge;\nwindow.dispatchEvent(new Event(\"ab.BridgeReady\"));\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/BrazeActivityLifecycleCallbackListener.kt",
    "content": "package com.braze\n\nimport android.app.Activity\nimport android.app.Application\nimport android.app.Application.ActivityLifecycleCallbacks\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.annotation.VisibleForTesting\nimport com.braze.push.NotificationTrampolineActivity\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\n\n/**\n * Can be used to automatically handle Braze lifecycle methods.\n * Optionally, openSession() and closeSession() are called on onActivityStarted and onActivityStopped respectively.\n * The InAppMessageManager methods of registerInAppMessageManager() and unregisterInAppMessageManager() can be optionally\n * called here as well.\n * Note: This callback should not be set in any Activity. It must be set in the Application class of your app.\n *\n * @param sessionHandlingEnabled              When true, handles calling openSession and closeSession in onActivityStarted\n * and onActivityStopped respectively.\n * @param registerInAppMessageManager         When true, registers and unregisters the [BrazeInAppMessageManager] in\n * [Application.ActivityLifecycleCallbacks.onActivityResumed] and [Application.ActivityLifecycleCallbacks.onActivityPaused]\n * respectively.\n * @param inAppMessagingRegistrationBlocklist A set of [Activity]s for which in-app message registration will not occur.\n * Each class should be retrieved via [Activity.getClass]. If null, an empty set is used instead.\n * @param sessionHandlingBlocklist            A set of [Activity]s for which session handling\n * will not occur. Each class should be retrieved via [Activity.getClass].\n * If null, an empty set is used instead.\n */\n@Suppress(\"TooManyFunctions\")\nopen class BrazeActivityLifecycleCallbackListener @JvmOverloads constructor(\n    private val sessionHandlingEnabled: Boolean = true,\n    private val registerInAppMessageManager: Boolean = true,\n    inAppMessagingRegistrationBlocklist: Set<Class<*>?>? = emptySet<Class<*>>(),\n    sessionHandlingBlocklist: Set<Class<*>?>? = emptySet<Class<*>>()\n) : ActivityLifecycleCallbacks {\n    private var inAppMessagingRegistrationBlocklist: Set<Class<*>?>\n    private var sessionHandlingBlocklist: Set<Class<*>?>\n\n    init {\n        this.inAppMessagingRegistrationBlocklist = inAppMessagingRegistrationBlocklist\n            ?: emptySet<Class<*>>()\n        this.sessionHandlingBlocklist = sessionHandlingBlocklist ?: emptySet<Class<*>>()\n        brazelog(V) {\n            \"BrazeActivityLifecycleCallbackListener using in-app messaging blocklist: ${this.inAppMessagingRegistrationBlocklist}\"\n        }\n        brazelog(V) {\n            \"BrazeActivityLifecycleCallbackListener using session handling blocklist: ${this.sessionHandlingBlocklist}\"\n        }\n    }\n\n    /**\n     * Constructor that sets a blocklist for session handling and [BrazeInAppMessageManager] registration while also\n     * enabling both features.\n     *\n     * @param inAppMessagingRegistrationBlocklist A set of [Activity]s for which in-app message registration will not\n     * occur. Each class should be retrieved via [Activity.getClass].\n     * @param sessionHandlingBlocklist            A set of [Activity]s for which session handling will not occur. Each\n     * class should be retrieved via [Activity.getClass].\n     */\n    @JvmOverloads\n    constructor(\n        inAppMessagingRegistrationBlocklist: Set<Class<*>?>?,\n        sessionHandlingBlocklist: Set<Class<*>?>? = emptySet<Class<*>>()\n    ) : this(true, true, inAppMessagingRegistrationBlocklist, sessionHandlingBlocklist)\n\n    /**\n     * Sets the [Activity.getClass] blocklist for which in-app message registration will not occur.\n     */\n    fun setInAppMessagingRegistrationBlocklist(blocklist: Set<Class<*>?>) {\n        brazelog(V) { \"setInAppMessagingRegistrationBlocklist called with blocklist: $blocklist\" }\n        inAppMessagingRegistrationBlocklist = blocklist\n    }\n\n    /**\n     * Sets the [Activity.getClass] blocklist for which session handling will not occur.\n     */\n    fun setSessionHandlingBlocklist(blocklist: Set<Class<*>?>) {\n        brazelog(V) { \"setSessionHandlingBlocklist called with blocklist: $blocklist\" }\n        sessionHandlingBlocklist = blocklist\n    }\n\n    override fun onActivityStarted(activity: Activity) {\n        if (sessionHandlingEnabled && shouldHandleLifecycleMethodsInActivity(activity, true)) {\n            brazelog(V) {\n                \"Automatically calling lifecycle method: openSession for class: ${activity.javaClass}\"\n            }\n            Braze.getInstance(activity.applicationContext).openSession(activity)\n        }\n    }\n\n    override fun onActivityStopped(activity: Activity) {\n        if (sessionHandlingEnabled && shouldHandleLifecycleMethodsInActivity(activity, true)) {\n            brazelog(V) {\n                \"Automatically calling lifecycle method: closeSession for class: ${activity.javaClass}\"\n            }\n            Braze.getInstance(activity.applicationContext).closeSession(activity)\n        }\n    }\n\n    override fun onActivityResumed(activity: Activity) {\n        if (registerInAppMessageManager &&\n            shouldHandleLifecycleMethodsInActivity(activity, false)\n        ) {\n            brazelog(V) {\n                \"Automatically calling lifecycle method: registerInAppMessageManager for class: ${activity.javaClass}\"\n            }\n            BrazeInAppMessageManager.getInstance().registerInAppMessageManager(activity)\n        }\n    }\n\n    override fun onActivityPaused(activity: Activity) {\n        if (registerInAppMessageManager &&\n            shouldHandleLifecycleMethodsInActivity(activity, false)\n        ) {\n            brazelog(V) {\n                \"Automatically calling lifecycle method: unregisterInAppMessageManager for class: ${activity.javaClass}\"\n            }\n            BrazeInAppMessageManager.getInstance().unregisterInAppMessageManager(activity)\n        }\n    }\n\n    override fun onActivityCreated(activity: Activity, bundle: Bundle?) {\n        if (registerInAppMessageManager &&\n            shouldHandleLifecycleMethodsInActivity(activity, false)\n        ) {\n            brazelog(V) {\n                \"Automatically calling lifecycle method: ensureSubscribedToInAppMessageEvents for class: ${activity.javaClass}\"\n            }\n            BrazeInAppMessageManager.getInstance()\n                .ensureSubscribedToInAppMessageEvents(activity.applicationContext)\n        }\n    }\n\n    override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}\n    override fun onActivityDestroyed(activity: Activity) {}\n\n    /**\n     * Determines if this [Activity] should be ignored for the purposes of session tracking or in-app message registration.\n     */\n    @VisibleForTesting\n    fun shouldHandleLifecycleMethodsInActivity(\n        activity: Activity,\n        forSessionHandling: Boolean\n    ): Boolean {\n        val activityClass: Class<out Activity> = activity.javaClass\n        if (activityClass == NotificationTrampolineActivity::class.java) {\n            brazelog(V) { \"Skipping automatic registration for notification trampoline activity class.\" }\n            // Always ignore\n            return false\n        }\n        return if (forSessionHandling) {\n            !sessionHandlingBlocklist.contains(activityClass)\n        } else {\n            !inAppMessagingRegistrationBlocklist.contains(activityClass)\n        }\n    }\n\n    /**\n     * Registers this listener directly against the Application.\n     * Equivalent to:\n     *\n     * ```\n     * applicationInstance.registerActivityLifecycleCallbacks(thisListener)\n     * ```\n     */\n    fun registerOnApplication(context: Context) {\n        try {\n            (context.applicationContext as Application).registerActivityLifecycleCallbacks(this)\n        } catch (e: Exception) {\n            brazelog(priority = E, e) { \"Failed to register this lifecycle callback listener directly against application class\" }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/IBrazeDeeplinkHandler.kt",
    "content": "package com.braze\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Bundle\nimport com.braze.enums.Channel\nimport com.braze.push.NotificationTrampolineActivity\nimport com.braze.ui.actions.NewsfeedAction\nimport com.braze.ui.actions.UriAction\n\n/**\n * This class defines the actions that should be taken when Braze attempts to follow a deeplink.\n */\ninterface IBrazeDeeplinkHandler {\n    enum class IntentFlagPurpose {\n        /**\n         * Used for notification actions using a deeplink.\n         */\n        NOTIFICATION_ACTION_WITH_DEEPLINK,\n\n        /**\n         * Used when generating the intent to the [NotificationTrampolineActivity]\n         * on a Push Story page traversal.\n         */\n        NOTIFICATION_PUSH_STORY_PAGE_CLICK,\n\n        /**\n         * Used in the default [UriAction] when opening\n         * a deeplink with the WebView activity.\n         */\n        URI_ACTION_OPEN_WITH_WEBVIEW_ACTIVITY,\n\n        /**\n         * Used in the default [UriAction] when opening\n         * a deeplink with [android.content.Intent.ACTION_VIEW].\n         */\n        URI_ACTION_OPEN_WITH_ACTION_VIEW,\n\n        /**\n         * Used in the default [UriAction] when creating the\n         * root backstack activity when opening a deeplink from a\n         * push notification.\n         */\n        URI_ACTION_BACK_STACK_GET_ROOT_INTENT,\n\n        /**\n         * Used in the default [UriAction] when creating the\n         * the backstack only contains the target intent and no\n         * root intent.\n         */\n        URI_ACTION_BACK_STACK_ONLY_GET_TARGET_INTENT,\n\n        /**\n         * Used in push notifications when only opening the main\n         * Activity and not a deeplink.\n         */\n        URI_UTILS_GET_MAIN_ACTIVITY_INTENT\n    }\n\n    /**\n     * This delegate method will be called when Braze wants to display the news feed.\n     *\n     * This method should implement the necessary logic to navigate to the to the Braze news feed\n     * that is integrated into the app.\n     *\n     * @param context        The current context.\n     * @param newsfeedAction The news feed action to execute.\n     */\n    fun gotoNewsFeed(context: Context, newsfeedAction: NewsfeedAction)\n\n    /**\n     * This delegate method will be called when Braze wants to navigate to a particular URI. If an\n     * IBrazeDeeplinkHandler is set, this method will be called instead of the default method (which\n     * is defined in [BrazeDeeplinkHandler].\n     *\n     * @param context   The current context.\n     * @param uriAction The Uri action to execute.\n     */\n    fun gotoUri(context: Context, uriAction: UriAction)\n\n    /**\n     * Get the flag mask used for [android.content.Intent.setFlags] based\n     * on the Intent usage.\n     */\n    fun getIntentFlags(intentFlagPurpose: IntentFlagPurpose): Int\n\n    /**\n     * Convenience method for creating [UriAction] instances. Returns null if the supplied url\n     * is blank or can not be parsed into a valid Uri.\n     */\n    fun createUriActionFromUrlString(url: String, extras: Bundle?, openInWebView: Boolean, channel: Channel): UriAction?\n\n    /**\n     * Convenience method for creating [UriAction] instances.\n     */\n    fun createUriActionFromUri(uri: Uri, extras: Bundle?, openInWebView: Boolean, channel: Channel): UriAction\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeAmazonDeviceMessagingReceiver.kt",
    "content": "package com.braze.push\n\nclass BrazeAmazonDeviceMessagingReceiver : BrazePushReceiver()\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeFirebaseMessagingService.kt",
    "content": "package com.braze.push\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.Braze\nimport com.braze.BrazeInternal.applyPendingRuntimeConfiguration\nimport com.braze.Constants\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.google.firebase.messaging.FirebaseMessagingService\nimport com.google.firebase.messaging.RemoteMessage\n\nopen class BrazeFirebaseMessagingService : FirebaseMessagingService() {\n\n    override fun onNewToken(newToken: String) {\n        super.onNewToken(newToken)\n        applyPendingRuntimeConfiguration(this)\n        val configurationProvider = BrazeConfigurationProvider(this)\n        if (Braze.getConfiguredApiKey(configurationProvider).isNullOrEmpty()) {\n            brazelog(V) { \"No configured API key, not registering token in onNewToken. Token: $newToken\" }\n            return\n        }\n        if (!configurationProvider.isFirebaseMessagingServiceOnNewTokenRegistrationEnabled) {\n            brazelog(V) {\n                \"Automatic FirebaseMessagingService.OnNewToken() registration\" +\n                    \" disabled, not registering token: $newToken\"\n            }\n            return\n        }\n        brazelog(V) { \"Registering Firebase push token in onNewToken. Token: $newToken\" }\n        Braze.getInstance(this).registeredPushToken = newToken\n    }\n\n    override fun onMessageReceived(remoteMessage: RemoteMessage) {\n        super.onMessageReceived(remoteMessage)\n        handleBrazeRemoteMessage(this, remoteMessage)\n    }\n\n    companion object {\n        /**\n         * Consumes an incoming [RemoteMessage] if it originated from Braze. If the [RemoteMessage] did\n         * not originate from Braze, then this method does nothing and returns false.\n         *\n         * @param remoteMessage The [RemoteMessage] from Firebase.\n         * @return true iff the [RemoteMessage] originated from Braze and was consumed. Returns false\n         * if the [RemoteMessage] did not originate from Braze or otherwise could not be forwarded.\n         */\n        @JvmStatic\n        fun handleBrazeRemoteMessage(context: Context, remoteMessage: RemoteMessage): Boolean {\n            if (!isBrazePushNotification(remoteMessage)) {\n                brazelog(I) { \"Remote message did not originate from Braze. Not consuming remote message: $remoteMessage\" }\n                return false\n            }\n            val remoteMessageData = remoteMessage.data\n            brazelog(I) { \"Got remote message from FCM: $remoteMessageData\" }\n            val pushIntent = Intent(BrazePushReceiver.FIREBASE_MESSAGING_SERVICE_ROUTING_ACTION)\n            val bundle = Bundle()\n            for ((key, value) in remoteMessageData) {\n                brazelog(V) { \"Adding bundle item from FCM remote data with key: $key and value: $value\" }\n                bundle.putString(key, value)\n            }\n            pushIntent.putExtras(bundle)\n            BrazePushReceiver.handleReceivedIntent(context, pushIntent)\n            return true\n        }\n\n        /**\n         * Determines if the Firebase [RemoteMessage] originated from Braze and should be\n         * forwarded to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage].\n         *\n         * @param remoteMessage The [RemoteMessage] from [FirebaseMessagingService.onMessageReceived]\n         * @return true iff this [RemoteMessage] originated from Braze or otherwise\n         * should be passed to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage].\n         */\n        @JvmStatic\n        fun isBrazePushNotification(remoteMessage: RemoteMessage): Boolean {\n            val remoteMessageData = remoteMessage.data\n            return \"true\" == remoteMessageData[Constants.BRAZE_PUSH_BRAZE_KEY]\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeHuaweiPushHandler.kt",
    "content": "package com.braze.push\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.Constants\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.toBundle\n\nobject BrazeHuaweiPushHandler {\n    /**\n     * Consumes an incoming data payload via Huawei if it originated from Braze. If the data payload did\n     * not originate from Braze, then this method does nothing and returns false.\n     *\n     * @param context Application context\n     * @param hmsRemoteMessageData The data payload map.\n     * @return true iff the Huawei data payload originated from Braze and was consumed. Returns false\n     * if the data payload did not originate from Braze or otherwise could not be forwarded or processed.\n     */\n    @JvmStatic\n    fun handleHmsRemoteMessageData(\n        context: Context,\n        hmsRemoteMessageData: Map<String, String>?\n    ): Boolean {\n        brazelog(V) { \"Handling Huawei remote message: $hmsRemoteMessageData\" }\n\n        // This could be null because that's what Huawei's interface has and this maintains\n        // backwards compatibility\n        if (hmsRemoteMessageData.isNullOrEmpty()) {\n            brazelog(W) { \"Remote message data was null. Remote message did not originate from Braze.\" }\n            return false\n        }\n\n        // Convert to a bundle for the intent passing\n        val bundle: Bundle = hmsRemoteMessageData.toBundle()\n        if (!bundle.containsKey(Constants.BRAZE_PUSH_BRAZE_KEY) || \"true\" != bundle.getString(Constants.BRAZE_PUSH_BRAZE_KEY)) {\n            brazelog(I) { \"Remote message did not originate from Braze. Not consuming remote message\" }\n            return false\n        }\n        brazelog(I) { \"Got remote message from Huawei: $bundle\" }\n        val pushIntent = Intent(BrazePushReceiver.HMS_PUSH_SERVICE_ROUTING_ACTION)\n        pushIntent.putExtras(bundle)\n        BrazePushReceiver.handleReceivedIntent(context, pushIntent)\n        return true\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeNotificationActionUtils.kt",
    "content": "package com.braze.push\n\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.core.app.NotificationCompat\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.models.push.BrazeNotificationPayload.ActionButton\nimport com.braze.Braze\nimport com.braze.Constants\nimport com.braze.IBrazeDeeplinkHandler.IntentFlagPurpose\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.push.BrazeNotificationUtils.cancelNotification\nimport com.braze.push.BrazeNotificationUtils.notificationReceiverClass\nimport com.braze.push.BrazeNotificationUtils.routeUserWithNotificationOpenedIntent\nimport com.braze.push.BrazeNotificationUtils.sendNotificationOpenedBroadcast\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.IntentUtils.getImmutablePendingIntentFlags\nimport com.braze.support.IntentUtils.getRequestCode\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance\n\nobject BrazeNotificationActionUtils {\n    /**\n     * Add notification actions to the provided notification builder.\n     */\n    @JvmStatic\n    fun addNotificationActions(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        if (payload.context == null) {\n            brazelog { \"Context cannot be null when adding notification buttons.\" }\n            return\n        }\n        val actionButtons = payload.actionButtons\n        if (actionButtons.isEmpty()) {\n            brazelog { \"No action buttons present. Not adding notification actions\" }\n            return\n        }\n        for (actionButton in actionButtons) {\n            brazelog(V) { \"Adding action button: $actionButton\" }\n            addNotificationAction(notificationBuilder, payload, actionButton)\n        }\n    }\n\n    /**\n     * Handles clicks on notification action buttons in the notification center. Called by FCM/ADM\n     * receiver when an Braze notification action button is clicked. The FCM/ADM receiver passes on\n     * the intent from the notification action button click intent.\n     *\n     * @param context [Context]\n     * @param intent the action button click intent\n     */\n    @JvmStatic\n    @Suppress(\"NestedBlockDepth\")\n    fun handleNotificationActionClicked(context: Context, intent: Intent) {\n        try {\n            val actionType = intent.getStringExtra(Constants.BRAZE_ACTION_TYPE_KEY)\n            if (actionType.isNullOrBlank()) {\n                brazelog(W) { \"Notification action button type was blank or null. Doing nothing.\" }\n                return\n            }\n            val notificationId = intent.getIntExtra(\n                Constants.BRAZE_PUSH_NOTIFICATION_ID,\n                Constants.BRAZE_DEFAULT_NOTIFICATION_ID\n            )\n\n            // Logs that the notification action was clicked.\n            // Click analytics for all action types are logged.\n            logNotificationActionClicked(context, intent, actionType)\n            if (actionType == Constants.BRAZE_PUSH_ACTION_TYPE_URI || actionType == Constants.BRAZE_PUSH_ACTION_TYPE_OPEN) {\n                cancelNotification(context, notificationId)\n                if (actionType == Constants.BRAZE_PUSH_ACTION_TYPE_URI &&\n                    intent.extras?.containsKey(Constants.BRAZE_ACTION_URI_KEY) == true\n                ) {\n                    // Set the deep link that to open to the correct action's deep link.\n                    intent.putExtra(\n                        Constants.BRAZE_PUSH_DEEP_LINK_KEY,\n                        intent.getStringExtra(Constants.BRAZE_ACTION_URI_KEY)\n                    )\n                    if (intent.extras?.containsKey(Constants.BRAZE_ACTION_USE_WEBVIEW_KEY) == true) {\n                        intent.putExtra(\n                            Constants.BRAZE_PUSH_OPEN_URI_IN_WEBVIEW_KEY,\n                            intent.getStringExtra(Constants.BRAZE_ACTION_USE_WEBVIEW_KEY)\n                        )\n                    }\n                } else {\n                    // Otherwise, remove any existing deep links.\n                    intent.removeExtra(Constants.BRAZE_PUSH_DEEP_LINK_KEY)\n                }\n                sendNotificationOpenedBroadcast(context, intent)\n                val appConfigurationProvider = BrazeConfigurationProvider(context)\n                if (appConfigurationProvider.doesHandlePushDeepLinksAutomatically) {\n                    routeUserWithNotificationOpenedIntent(context, intent)\n                } else {\n                    brazelog(I) { \"Not handling deep links automatically, skipping deep link handling\" }\n                }\n            } else if (actionType == Constants.BRAZE_PUSH_ACTION_TYPE_NONE) {\n                cancelNotification(context, notificationId)\n            } else {\n                brazelog(W) { \"Unknown notification action button clicked. Doing nothing.\" }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Caught exception while handling notification action button click.\" }\n        }\n    }\n\n    /**\n     * Add the notification action at a specified index to the notification builder.\n     *\n     * @param notificationBuilder\n     * @param payload\n     * @param actionButton that is to be added at actionButton.actionIndex\n     */\n    fun addNotificationAction(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload,\n        actionButton: ActionButton\n    ) {\n        val context = payload.context\n        if (context == null) {\n            brazelog { \"Cannot add notification action with null context from payload\" }\n            return\n        }\n        val actionExtras = Bundle(payload.notificationExtras)\n        actionButton.putIntoBundle(actionExtras)\n        val actionType = actionButton.type\n\n        val pendingSendIntent: PendingIntent\n        val sendIntent: Intent\n        val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or getImmutablePendingIntentFlags()\n        if (Constants.BRAZE_PUSH_ACTION_TYPE_NONE == actionType) {\n            // If no action is present, then we don't need the\n            // trampoline to route us back to an Activity.\n            brazelog(V) {\n                \"Adding notification action with type: $actionType\" +\n                    \"Setting intent class to notification receiver: $notificationReceiverClass\"\n            }\n            sendIntent = Intent(Constants.BRAZE_ACTION_CLICKED_ACTION).setClass(\n                context,\n                notificationReceiverClass\n            )\n            sendIntent.putExtras(actionExtras)\n            pendingSendIntent = PendingIntent.getBroadcast(\n                context,\n                getRequestCode(),\n                sendIntent,\n                pendingIntentFlags\n            )\n        } else {\n            // However, if an action is present, then we need to\n            // route to the trampoline to ensure the user is\n            // prompted to open the app on the lockscreen.\n            brazelog(V) { \"Adding notification action with type: $actionType Setting intent class to trampoline activity\" }\n            sendIntent = Intent(Constants.BRAZE_ACTION_CLICKED_ACTION)\n                .setClass(context, NotificationTrampolineActivity::class.java)\n            sendIntent.flags =\n                sendIntent.flags or getInstance().getIntentFlags(IntentFlagPurpose.NOTIFICATION_ACTION_WITH_DEEPLINK)\n            sendIntent.putExtras(actionExtras)\n            pendingSendIntent = PendingIntent.getActivity(\n                context,\n                getRequestCode(),\n                sendIntent,\n                pendingIntentFlags\n            )\n        }\n        val notificationActionBuilder =\n            NotificationCompat.Action.Builder(0, actionButton.text, pendingSendIntent)\n        notificationActionBuilder.addExtras(Bundle(actionExtras))\n        notificationBuilder.addAction(notificationActionBuilder.build())\n        brazelog(V) { \"Added action with bundle: $actionExtras\" }\n    }\n\n    /**\n     * Log an action button clicked event.\n     *\n     * @param context\n     * @param intent the action button click intent\n     */\n    fun logNotificationActionClicked(context: Context, intent: Intent, actionType: String?) {\n        val campaignId = intent.getStringExtra(Constants.BRAZE_PUSH_CAMPAIGN_ID_KEY)\n        val actionButtonId = intent.getStringExtra(Constants.BRAZE_ACTION_ID_KEY)\n        Braze.getInstance(context)\n            .logPushNotificationActionClicked(campaignId, actionButtonId, actionType)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeNotificationFactory.kt",
    "content": "package com.braze.push\n\nimport android.app.Notification\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.core.app.NotificationCompat\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.IBrazeNotificationFactory\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.push.BrazeNotificationActionUtils.addNotificationActions\nimport com.braze.push.BrazeNotificationStyleFactory.Companion.setStyleIfSupported\nimport com.braze.push.BrazeNotificationUtils.getOrCreateNotificationChannelId\nimport com.braze.push.BrazeNotificationUtils.prefetchBitmapsIfNewlyReceivedStoryPush\nimport com.braze.push.BrazeNotificationUtils.setAccentColorIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setCategoryIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setContentIfPresent\nimport com.braze.push.BrazeNotificationUtils.setContentIntentIfPresent\nimport com.braze.push.BrazeNotificationUtils.setDeleteIntent\nimport com.braze.push.BrazeNotificationUtils.setLargeIconIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setNotificationBadgeNumberIfPresent\nimport com.braze.push.BrazeNotificationUtils.setPriorityIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setPublicVersionIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setSetShowWhen\nimport com.braze.push.BrazeNotificationUtils.setSmallIcon\nimport com.braze.push.BrazeNotificationUtils.setSoundIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setSummaryTextIfPresentAndSupported\nimport com.braze.push.BrazeNotificationUtils.setTickerIfPresent\nimport com.braze.push.BrazeNotificationUtils.setTitleIfPresent\nimport com.braze.push.BrazeNotificationUtils.setVisibilityIfPresentAndSupported\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\n\nopen class BrazeNotificationFactory : IBrazeNotificationFactory {\n    /**\n     * Creates the rich notification. The notification content varies based on the Android version on the\n     * device, but each notification can contain an icon, image, title, and content.\n     *\n     * Opening a notification from the notification center triggers a broadcast message to be sent.\n     * The broadcast message action is com.braze.push.intent.NOTIFICATION_OPENED.\n     */\n    override fun createNotification(payload: BrazeNotificationPayload): Notification? {\n        val builder = populateNotificationBuilder(payload)\n        return if (builder != null) {\n            builder.build()\n        } else {\n            brazelog(I) { \"Notification could not be built. Returning null as created notification\" }\n            null\n        }\n    }\n\n    /**\n     * Please use [createNotification] directly instead.\n     */\n    fun createNotification(\n        appConfigurationProvider: BrazeConfigurationProvider?,\n        context: Context?,\n        notificationExtras: Bundle?,\n        brazeExtras: Bundle?\n    ): Notification? {\n        val payload = BrazeNotificationPayload(\n            notificationExtras,\n            brazeExtras,\n            context,\n            appConfigurationProvider\n        )\n        return createNotification(payload)\n    }\n\n    /**\n     * Equivalent to [createNotification].\n     */\n    fun populateNotificationBuilder(\n        configurationProvider: BrazeConfigurationProvider?,\n        context: Context?,\n        notificationExtras: Bundle?,\n        brazeExtras: Bundle?\n    ): NotificationCompat.Builder? {\n        val payload = BrazeNotificationPayload(\n            notificationExtras,\n            brazeExtras,\n            context,\n            configurationProvider\n        )\n        return populateNotificationBuilder(payload)\n    }\n\n    companion object {\n        private val internalInstance: BrazeNotificationFactory = BrazeNotificationFactory()\n\n        /**\n         * Returns the singleton [BrazeNotificationFactory] instance.\n         */\n        @JvmStatic\n        val instance: BrazeNotificationFactory\n            get() {\n                return internalInstance\n            }\n\n        /**\n         * Returns a notification builder populated with all fields from the notification extras and\n         * Braze extras.\n         *\n         * To create a notification object, call `build()` on the returned builder instance.\n         */\n        @JvmStatic\n        fun populateNotificationBuilder(payload: BrazeNotificationPayload): NotificationCompat.Builder? {\n            brazelog(V) { \"Using BrazeNotificationPayload: $payload\" }\n            val context = payload.context\n            if (context == null) {\n                brazelog { \"BrazeNotificationPayload has null context. Not creating notification\" }\n                return null\n            }\n            val brazeConfigurationProvider = payload.configurationProvider\n            if (brazeConfigurationProvider == null) {\n                brazelog { \"BrazeNotificationPayload has null app configuration provider. Not creating notification\" }\n                return null\n            }\n            val notificationExtras = payload.notificationExtras\n\n            // We build up the notification by setting values if they are present in the extras and supported\n            // on the device. The notification building is currently order/combination independent, but\n            // the addition of new RemoteViews options could mean that some methods conflict/overwrite. For clarity\n            // we build the notification up in the order that each feature was supported.\n\n            // If this notification is a push story,\n            // make a best effort to preload bitmap images into the cache.\n            prefetchBitmapsIfNewlyReceivedStoryPush(payload)\n            val notificationChannelId = getOrCreateNotificationChannelId(payload)\n            val notificationBuilder =\n                NotificationCompat.Builder(context, notificationChannelId)\n                    .setAutoCancel(true)\n            setTitleIfPresent(notificationBuilder, payload)\n            setContentIfPresent(notificationBuilder, payload)\n            setTickerIfPresent(notificationBuilder, payload)\n            setSetShowWhen(notificationBuilder, payload)\n\n            // Add intent to fire when the notification is opened or deleted.\n            setContentIntentIfPresent(context, notificationBuilder, notificationExtras)\n            setDeleteIntent(context, notificationBuilder, notificationExtras)\n            setSmallIcon(brazeConfigurationProvider, notificationBuilder)\n            setLargeIconIfPresentAndSupported(notificationBuilder, payload)\n            setSoundIfPresentAndSupported(notificationBuilder, payload)\n\n            // Subtext, priority, notification actions, and styles were added in JellyBean.\n            setSummaryTextIfPresentAndSupported(notificationBuilder, payload)\n            setPriorityIfPresentAndSupported(notificationBuilder, payload)\n            setStyleIfSupported(notificationBuilder, payload)\n            addNotificationActions(notificationBuilder, payload)\n\n            // Accent color, category, visibility, and public notification were added in Lollipop.\n            setAccentColorIfPresentAndSupported(notificationBuilder, payload)\n            setCategoryIfPresentAndSupported(notificationBuilder, payload)\n            setVisibilityIfPresentAndSupported(notificationBuilder, payload)\n            setPublicVersionIfPresentAndSupported(notificationBuilder, payload)\n\n            // Notification priority and sound were deprecated in Android O\n            setNotificationBadgeNumberIfPresent(notificationBuilder, payload)\n            return notificationBuilder\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeNotificationStyleFactory.kt",
    "content": "package com.braze.push\n\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageManager\nimport android.graphics.Bitmap\nimport android.graphics.drawable.Icon\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.RemoteViews\nimport androidx.annotation.RequiresApi\nimport androidx.annotation.VisibleForTesting\nimport androidx.core.app.NotificationCompat\nimport com.braze.Braze\nimport com.braze.Constants\nimport com.braze.IBrazeDeeplinkHandler.IntentFlagPurpose\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.BrazeDateFormat\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.models.push.BrazeNotificationPayload.PushStoryPage\nimport com.braze.push.BrazeNotificationUtils.getNotificationId\nimport com.braze.push.support.getHtmlSpannedTextIfEnabled\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.IntentUtils.getImmutablePendingIntentFlags\nimport com.braze.support.IntentUtils.getRequestCode\nimport com.braze.support.formatDateNow\nimport com.braze.support.getDensityDpi\nimport com.braze.support.getDisplayWidthPixels\nimport com.braze.support.getPixelsFromDensityAndDp\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance\nimport com.braze.ui.R\n\nopen class BrazeNotificationStyleFactory {\n    /**\n     * A sentinel value used solely to denote that a style\n     * should not be set on the notification builder.\n     *\n     * Example usage would be a fully custom [RemoteViews]\n     * that has handled rendering notification view without the\n     * use of a system style. Returning null in that scenario\n     * would lead to a lack of information as to whether that\n     * custom rendering failed.\n     */\n    private class NoOpSentinelStyle : NotificationCompat.Style()\n\n    companion object {\n        /**\n         * BigPictureHeight is set in notification_template_big_picture.xml.\n         */\n        private const val BIG_PICTURE_STYLE_IMAGE_HEIGHT = 192\n        private const val STORY_SET_GRAVITY = \"setGravity\"\n        private const val STORY_SET_VISIBILITY = \"setVisibility\"\n\n        /**\n         * Sets the style of the notification if supported.\n         *\n         * If there is an image url found in the extras payload and the image can be downloaded, then\n         * use the android BigPictureStyle as the notification. Else, use the BigTextStyle instead.\n         */\n        @JvmStatic\n        fun setStyleIfSupported(\n            notificationBuilder: NotificationCompat.Builder,\n            payload: BrazeNotificationPayload\n        ) {\n            val style = getNotificationStyle(notificationBuilder, payload)\n            if (style !is NoOpSentinelStyle) {\n                brazelog { \"Setting style for notification\" }\n                notificationBuilder.setStyle(style)\n            }\n        }\n\n        /**\n         * Returns a big style [NotificationCompat.Style]. If an image is present, this will be a [NotificationCompat.BigPictureStyle],\n         * otherwise it will be a [NotificationCompat.BigTextStyle].\n         */\n        fun getNotificationStyle(\n            notificationBuilder: NotificationCompat.Builder,\n            payload: BrazeNotificationPayload\n        ): NotificationCompat.Style {\n            var style: NotificationCompat.Style? = null\n            if (payload.isPushStory && payload.context != null) {\n                brazelog { \"Rendering push notification with DecoratedCustomViewStyle (Story)\" }\n                style = getStoryStyle(notificationBuilder, payload)\n            } else if (payload.isConversationalPush && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {\n                brazelog { \"Rendering conversational push\" }\n                style = getConversationalPushStyle(notificationBuilder, payload)\n            } else if (payload.bigImageUrl != null) {\n                style =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && payload.isInlineImagePush) {\n                        brazelog { \"Rendering push notification with custom inline image style\" }\n                        getInlineImageStyle(payload, notificationBuilder)\n                    } else {\n                        brazelog { \"Rendering push notification with BigPictureStyle\" }\n                        getBigPictureNotificationStyle(payload)\n                    }\n            }\n\n            // Default style is BigTextStyle.\n            if (style == null) {\n                brazelog { \"Rendering push notification with BigTextStyle\" }\n                style = getBigTextNotificationStyle(payload)\n            }\n            return style\n        }\n\n        /**\n         * Returns a [NotificationCompat.BigTextStyle] notification style initialized with the content, big title, and big summary\n         * specified in the notificationExtras and brazeExtras bundles.\n         *\n         * If summary text exists, it will be shown in the expanded notification view.\n         * If a title exists, it will override the default in expanded notification view.\n         */\n        fun getBigTextNotificationStyle(payload: BrazeNotificationPayload): NotificationCompat.BigTextStyle {\n            val bigTextNotificationStyle = NotificationCompat.BigTextStyle()\n            val appConfigProvider = payload.configurationProvider ?: return bigTextNotificationStyle\n\n            payload.contentText?.getHtmlSpannedTextIfEnabled(appConfigProvider)?.let {\n                bigTextNotificationStyle.bigText(it)\n            }\n\n            payload.bigSummaryText?.let {\n                bigTextNotificationStyle.setSummaryText(\n                    it.getHtmlSpannedTextIfEnabled(\n                        appConfigProvider\n                    )\n                )\n            }\n\n            payload.bigTitleText?.let {\n                bigTextNotificationStyle.setBigContentTitle(\n                    it.getHtmlSpannedTextIfEnabled(\n                        appConfigProvider\n                    )\n                )\n            }\n            return bigTextNotificationStyle\n        }\n\n        /**\n         * Returns a [androidx.core.app.NotificationCompat.DecoratedCustomViewStyle] for push story.\n         *\n         * @param notificationBuilder\n         * @param payload\n         * @return a [androidx.core.app.NotificationCompat.DecoratedCustomViewStyle] that describes the appearance of the push story.\n         */\n        fun getStoryStyle(\n            notificationBuilder: NotificationCompat.Builder,\n            payload: BrazeNotificationPayload\n        ): NotificationCompat.DecoratedCustomViewStyle? {\n            val context = payload.context\n            if (context == null) {\n                brazelog { \"Push story page cannot render without a context\" }\n                return null\n            }\n            val pushStoryPages = payload.pushStoryPages\n            val pageIndex = payload.pushStoryPageIndex\n            val pushStoryPage = pushStoryPages[pageIndex]\n            val storyView =\n                RemoteViews(context.packageName, R.layout.com_braze_push_story_one_image)\n            if (!populatePushStoryPage(storyView, payload, pushStoryPage)) {\n                brazelog(W) { \"Push story page was not populated correctly. Not using DecoratedCustomViewStyle.\" }\n                return null\n            }\n            val notificationExtras = payload.notificationExtras\n            val style = NotificationCompat.DecoratedCustomViewStyle()\n            val numPages = pushStoryPages.size\n            val previousButtonPendingIntent = createStoryTraversedPendingIntent(\n                context,\n                notificationExtras,\n                (pageIndex - 1 + numPages) % numPages\n            )\n            storyView.setOnClickPendingIntent(\n                R.id.com_braze_story_button_previous,\n                previousButtonPendingIntent\n            )\n            val nextButtonPendingIntent = createStoryTraversedPendingIntent(\n                context,\n                notificationExtras,\n                (pageIndex + 1) % numPages\n            )\n            storyView.setOnClickPendingIntent(\n                R.id.com_braze_story_button_next,\n                nextButtonPendingIntent\n            )\n            notificationBuilder.setCustomBigContentView(storyView)\n\n            // Ensure clicks on the story don't vibrate or make noise after the story first appears\n            notificationBuilder.setOnlyAlertOnce(true)\n            return style\n        }\n\n        /**\n         * This method sets a fully custom [android.widget.RemoteViews.RemoteView] to render the\n         * notification.\n         *\n         * In the successful case, a [NoOpSentinelStyle] is returned.\n         * In the failure case (image bitmap is null, system information not found, etc.), a\n         * null style is returned.\n         */\n        @Suppress(\"LongMethod\", \"ReturnCount\")\n        @RequiresApi(api = Build.VERSION_CODES.M)\n        fun getInlineImageStyle(\n            payload: BrazeNotificationPayload,\n            notificationBuilder: NotificationCompat.Builder\n        ): NotificationCompat.Style? {\n            val context = payload.context\n            if (context == null) {\n                brazelog { \"Inline Image Push cannot render without a context\" }\n                return null\n            }\n            val imageUrl = payload.bigImageUrl\n            if (imageUrl.isNullOrBlank()) {\n                brazelog { \"Inline Image Push image url invalid\" }\n                return null\n            }\n            val notificationExtras = payload.notificationExtras\n\n            // Set the image\n            val largeNotificationBitmap = Braze.getInstance(context).imageLoader\n                .getPushBitmapFromUrl(\n                    context,\n                    notificationExtras,\n                    imageUrl,\n                    BrazeViewBounds.NOTIFICATION_INLINE_PUSH_IMAGE\n                )\n            if (largeNotificationBitmap == null) {\n                brazelog { \"Inline Image Push failed to get image bitmap\" }\n                return null\n            }\n            val isNotificationSpaceConstrained =\n                isRemoteViewNotificationAvailableSpaceConstrained(context)\n            val remoteView = RemoteViews(\n                context.packageName,\n                if (isNotificationSpaceConstrained) R.layout.com_braze_push_inline_image_constrained else R.layout.com_braze_notification_inline_image\n            )\n            val configurationProvider = BrazeConfigurationProvider(context)\n\n            // Set the app icon drawable\n            val appIcon = Icon.createWithResource(\n                context,\n                configurationProvider.smallNotificationIconResourceId\n            )\n\n            payload.accentColor?.let { color ->\n                appIcon.setTint(color)\n            }\n            remoteView.setImageViewIcon(R.id.com_braze_inline_image_push_app_icon, appIcon)\n\n            // Set the app name\n            val packageManager = context.packageManager\n            val applicationInfo: ApplicationInfo = try {\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    packageManager.getApplicationInfo(context.packageName, PackageManager.ApplicationInfoFlags.of(0))\n                } else {\n                    @Suppress(\"DEPRECATION\") packageManager.getApplicationInfo(context.packageName, 0)\n                }\n            } catch (e: PackageManager.NameNotFoundException) {\n                brazelog(E, e) { \"Inline Image Push application info was null\" }\n                return null\n            }\n            val applicationName = packageManager.getApplicationLabel(applicationInfo) as String\n            val htmlSpannedAppName =\n                applicationName.getHtmlSpannedTextIfEnabled(configurationProvider)\n            remoteView.setTextViewText(\n                R.id.com_braze_inline_image_push_app_name_text,\n                htmlSpannedAppName\n            )\n\n            // Set the current time\n            remoteView.setTextViewText(\n                R.id.com_braze_inline_image_push_time_text,\n                formatDateNow(BrazeDateFormat.CLOCK_12_HOUR)\n            )\n\n            // Set the text area title\n            notificationExtras.getString(Constants.BRAZE_PUSH_TITLE_KEY)?.let { title ->\n                remoteView.setTextViewText(\n                    R.id.com_braze_inline_image_push_title_text,\n                    title.getHtmlSpannedTextIfEnabled(configurationProvider)\n                )\n            }\n\n            // Set the text area content\n            notificationExtras.getString(Constants.BRAZE_PUSH_CONTENT_KEY)?.let { content ->\n                remoteView.setTextViewText(\n                    R.id.com_braze_inline_image_push_content_text,\n                    content.getHtmlSpannedTextIfEnabled(configurationProvider)\n                )\n            }\n            notificationBuilder.setCustomContentView(remoteView)\n            return if (isNotificationSpaceConstrained) {\n                // On Android 12 and above, the custom image view we had\n                // just can't render the same way so we'll fake it with a large icon\n                notificationBuilder.setLargeIcon(largeNotificationBitmap)\n                NotificationCompat.DecoratedCustomViewStyle()\n            } else {\n                remoteView.setImageViewBitmap(\n                    R.id.com_braze_inline_image_push_side_image,\n                    largeNotificationBitmap\n                )\n                // Since this is entirely custom, no decorated\n                // style is returned to the system.\n                NoOpSentinelStyle()\n            }\n        }\n\n        /**\n         * Returns a [NotificationCompat.BigPictureStyle] notification style initialized with the bitmap, big title, and big summary\n         * specified in the notificationExtras and brazeExtras bundles.\n         *\n         * If summary text exists, it will be shown in the expanded notification view.\n         * If a title exists, it will override the default in expanded notification view.\n         */\n        @Suppress(\"ReturnCount\")\n        fun getBigPictureNotificationStyle(payload: BrazeNotificationPayload): NotificationCompat.BigPictureStyle? {\n            val context = payload.context ?: return null\n            val imageUrl = payload.bigImageUrl\n            if (imageUrl.isNullOrBlank()) {\n                return null\n            }\n            val notificationExtras = payload.notificationExtras\n            var imageBitmap = Braze.getInstance(context).imageLoader\n                .getPushBitmapFromUrl(\n                    context,\n                    notificationExtras,\n                    imageUrl,\n                    BrazeViewBounds.NOTIFICATION_EXPANDED_IMAGE\n                )\n            if (imageBitmap == null) {\n                brazelog {\n                    \"Failed to download image bitmap for big picture notification style. Url: $imageUrl\"\n                }\n                return null\n            }\n            return try {\n                // Images get cropped differently across different screen sizes\n                // Here we grab the current screen size and scale the image to fit correctly\n                // Note: if the height is greater than the width it's going to look poor, so we might\n                // as well let the system modify it and not complicate things by trying to smoosh it here.\n                if (imageBitmap.width > imageBitmap.height) {\n                    val bigPictureHeightPixels = getPixelsFromDensityAndDp(\n                        getDensityDpi(context),\n                        BIG_PICTURE_STYLE_IMAGE_HEIGHT\n                    )\n                    // 2:1 aspect ratio\n                    var bigPictureWidthPixels = 2 * bigPictureHeightPixels\n                    val displayWidthPixels = getDisplayWidthPixels(context)\n                    if (bigPictureWidthPixels > displayWidthPixels) {\n                        bigPictureWidthPixels = displayWidthPixels\n                    }\n                    try {\n                        imageBitmap = Bitmap.createScaledBitmap(\n                            imageBitmap,\n                            bigPictureWidthPixels,\n                            bigPictureHeightPixels,\n                            true\n                        )\n                    } catch (e: Exception) {\n                        brazelog(E, e) { \"Failed to scale image bitmap, using original.\" }\n                    }\n                }\n                if (imageBitmap == null) {\n                    brazelog(I) {\n                        \"Bitmap download failed for push notification. No image will be included with the notification.\"\n                    }\n                    return null\n                }\n                val bigPictureNotificationStyle = NotificationCompat.BigPictureStyle()\n                bigPictureNotificationStyle.bigPicture(imageBitmap)\n                setBigPictureSummaryAndTitle(bigPictureNotificationStyle, payload)\n                bigPictureNotificationStyle\n            } catch (e: Exception) {\n                brazelog(E, e) { \"Failed to create Big Picture Style.\" }\n                null\n            }\n        }\n\n        fun getConversationalPushStyle(\n            notificationBuilder: NotificationCompat.Builder,\n            payload: BrazeNotificationPayload\n        ): NotificationCompat.MessagingStyle? {\n            return try {\n                val conversationPersonMap = payload.conversationPersonMap\n                val replyPerson = conversationPersonMap[payload.conversationReplyPersonId]\n                if (replyPerson == null) {\n                    brazelog { \"Reply person does not exist in mapping. Not rendering a style\" }\n                    return null\n                }\n                val style = NotificationCompat.MessagingStyle(replyPerson.person)\n                for (message in payload.conversationMessages) {\n                    val person = conversationPersonMap[message.personId]\n                    if (person == null) {\n                        brazelog {\n                            \"Message person does not exist in mapping. Not rendering a style. $message\"\n                        }\n                        return null\n                    }\n                    style.addMessage(message.message, message.timestamp, person.person)\n                }\n                style.isGroupConversation = conversationPersonMap.size > 1\n                notificationBuilder.setShortcutId(payload.conversationShortcutId)\n                style\n            } catch (e: Exception) {\n                brazelog(E, e) { \"Failed to create conversation push style. Returning null.\" }\n                null\n            }\n        }\n\n        private fun createStoryPageClickedPendingIntent(\n            context: Context,\n            payload: BrazeNotificationPayload,\n            pushStoryPage: PushStoryPage\n        ): PendingIntent {\n            val storyClickedIntent = Intent(Constants.BRAZE_STORY_CLICKED_ACTION)\n                .setClass(context, NotificationTrampolineActivity::class.java)\n            storyClickedIntent.flags =\n                storyClickedIntent.flags or getInstance().getIntentFlags(IntentFlagPurpose.NOTIFICATION_PUSH_STORY_PAGE_CLICK)\n            payload.notificationExtras.let {\n                storyClickedIntent.putExtras(it)\n            }\n            storyClickedIntent.putExtra(Constants.BRAZE_ACTION_URI_KEY, pushStoryPage.deeplink)\n            storyClickedIntent.putExtra(\n                Constants.BRAZE_ACTION_USE_WEBVIEW_KEY,\n                pushStoryPage.useWebview\n            )\n            storyClickedIntent.putExtra(Constants.BRAZE_STORY_PAGE_ID, pushStoryPage.storyPageId)\n            storyClickedIntent.putExtra(Constants.BRAZE_CAMPAIGN_ID, pushStoryPage.campaignId)\n            val notificationId = getNotificationId(payload)\n            storyClickedIntent.putExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID, notificationId)\n            return PendingIntent.getActivity(\n                context,\n                getRequestCode(),\n                storyClickedIntent,\n                getImmutablePendingIntentFlags()\n            )\n        }\n\n        private fun createStoryTraversedPendingIntent(\n            context: Context,\n            notificationExtras: Bundle?,\n            pageIndex: Int\n        ): PendingIntent {\n            val storyNextClickedIntent = Intent(Constants.BRAZE_STORY_TRAVERSE_CLICKED_ACTION)\n                .setClass(context, BrazeNotificationUtils.notificationReceiverClass)\n            if (notificationExtras != null) {\n                notificationExtras.putInt(Constants.BRAZE_STORY_INDEX_KEY, pageIndex)\n                storyNextClickedIntent.putExtras(notificationExtras)\n            }\n            val flags = PendingIntent.FLAG_ONE_SHOT or getImmutablePendingIntentFlags()\n            return PendingIntent.getBroadcast(\n                context,\n                getRequestCode(),\n                storyNextClickedIntent,\n                flags\n            )\n        }\n\n        /**\n         * Adds the appropriate image, title/subtitle, and PendingIntents to the story page.\n         *\n         * @param view               The push story remoteView, as instantiated in the getStoryStyle method.\n         * @param payload\n         * @param pushStoryPage\n         * @return True if the push story page was populated correctly.\n         */\n        @Suppress(\"LongMethod\", \"ReturnCount\")\n        private fun populatePushStoryPage(\n            view: RemoteViews,\n            payload: BrazeNotificationPayload,\n            pushStoryPage: PushStoryPage\n        ): Boolean {\n            val context = payload.context\n            if (context == null) {\n                brazelog { \"Push story page cannot render without a context\" }\n                return false\n            }\n            val configurationProvider = payload.configurationProvider\n            if (configurationProvider == null) {\n                brazelog { \"Push story page cannot render without a configuration provider\" }\n                return false\n            }\n            val bitmapUrl = pushStoryPage.bitmapUrl\n            if (bitmapUrl.isNullOrBlank()) {\n                brazelog { \"Push story page image url invalid\" }\n                return false\n            }\n            val notificationExtras = payload.notificationExtras\n\n            // Set up bitmap url\n            val largeNotificationBitmap = Braze.getInstance(context).imageLoader\n                .getPushBitmapFromUrl(\n                    context,\n                    notificationExtras,\n                    bitmapUrl,\n                    BrazeViewBounds.NOTIFICATION_ONE_IMAGE_STORY\n                )\n                ?: return false\n            view.setImageViewBitmap(R.id.com_braze_story_image_view, largeNotificationBitmap)\n\n            // Set up title\n            val pageTitle = pushStoryPage.title\n\n            // If the title is null or blank, the visibility of the container becomes GONE.\n            if (!pageTitle.isNullOrBlank()) {\n                val pageTitleText = pageTitle.getHtmlSpannedTextIfEnabled(configurationProvider)\n                view.setTextViewText(R.id.com_braze_story_text_view, pageTitleText)\n                val titleGravity = pushStoryPage.titleGravity\n                view.setInt(\n                    R.id.com_braze_story_text_view_container,\n                    STORY_SET_GRAVITY,\n                    titleGravity\n                )\n            } else {\n                view.setInt(\n                    R.id.com_braze_story_text_view_container,\n                    STORY_SET_VISIBILITY,\n                    View.GONE\n                )\n            }\n\n            // Set up subtitle\n            val pageSubtitle = pushStoryPage.subtitle\n\n            // If the subtitle is null or blank, the visibility of the container becomes GONE.\n            if (!pageSubtitle.isNullOrBlank()) {\n                val pageSubtitleText =\n                    pageSubtitle.getHtmlSpannedTextIfEnabled(configurationProvider)\n                view.setTextViewText(R.id.com_braze_story_text_view_small, pageSubtitleText)\n                val subtitleGravity = pushStoryPage.subtitleGravity\n                view.setInt(\n                    R.id.com_braze_story_text_view_small_container,\n                    STORY_SET_GRAVITY,\n                    subtitleGravity\n                )\n            } else {\n                view.setInt(\n                    R.id.com_braze_story_text_view_small_container,\n                    STORY_SET_VISIBILITY,\n                    View.GONE\n                )\n            }\n\n            // Set up story clicked intent\n            val storyClickedPendingIntent =\n                createStoryPageClickedPendingIntent(context, payload, pushStoryPage)\n            view.setOnClickPendingIntent(\n                R.id.com_braze_story_relative_layout,\n                storyClickedPendingIntent\n            )\n            return true\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        fun setBigPictureSummaryAndTitle(\n            bigPictureNotificationStyle: NotificationCompat.BigPictureStyle,\n            payload: BrazeNotificationPayload\n        ) {\n            val appConfigProvider = payload.configurationProvider ?: return\n            val bigSummaryText = payload.bigSummaryText\n            val bigTitleText = payload.bigTitleText\n            val summaryText = payload.summaryText\n\n            if (bigSummaryText != null) {\n                bigPictureNotificationStyle.setSummaryText(\n                    bigSummaryText.getHtmlSpannedTextIfEnabled(\n                        appConfigProvider\n                    )\n                )\n            }\n            if (bigTitleText != null) {\n                bigPictureNotificationStyle.setBigContentTitle(\n                    bigTitleText.getHtmlSpannedTextIfEnabled(\n                        appConfigProvider\n                    )\n                )\n            }\n\n            // If summary is null (which we set to the subtext in setSummaryTextIfPresentAndSupported in BrazeNotificationUtils)\n            // and bigSummary is null, set the summary to the message. Without this, the message would be blank in expanded mode.\n            if (summaryText == null && bigSummaryText == null) {\n                payload.contentText?.let {\n                    bigPictureNotificationStyle.setSummaryText(\n                        it.getHtmlSpannedTextIfEnabled(\n                            appConfigProvider\n                        )\n                    )\n                }\n            }\n        }\n\n        /**\n         * On an Android 12 device and app targeting Android 12, the available space to\n         * a [RemoteViews] notification is significantly reduced.\n         */\n        private fun isRemoteViewNotificationAvailableSpaceConstrained(context: Context): Boolean {\n            // Check that the device is on Android 12+ && the app is targeting Android 12+\n            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&\n                context.applicationContext.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.S\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazeNotificationUtils.kt",
    "content": "package com.braze.push\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.annotation.TargetApi\nimport android.app.AlarmManager\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.app.UiModeManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.PowerManager\nimport android.os.SystemClock\nimport androidx.core.app.NotificationCompat\nimport com.braze.Braze\nimport com.braze.BrazeInternal\nimport com.braze.BrazeInternal.addSerializedContentCardToStorage\nimport com.braze.BrazeInternal.refreshFeatureFlags\nimport com.braze.BrazeInternal.requestGeofenceRefresh\nimport com.braze.Constants\nimport com.braze.Constants.isAmazonDevice\nimport com.braze.IBrazeNotificationFactory\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.BrazePushEventType\nimport com.braze.enums.BrazePushEventType.NOTIFICATION_DELETED\nimport com.braze.enums.BrazePushEventType.NOTIFICATION_OPENED\nimport com.braze.enums.BrazePushEventType.NOTIFICATION_RECEIVED\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.enums.Channel\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.push.support.getHtmlSpannedTextIfEnabled\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport com.braze.support.IntentUtils.addComponentAndSendBroadcast\nimport com.braze.support.IntentUtils.getImmutablePendingIntentFlags\nimport com.braze.support.IntentUtils.getRequestCode\nimport com.braze.support.getOptionalString\nimport com.braze.support.hasPermission\nimport com.braze.support.parseJsonObjectIntoBundle\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance\nimport com.braze.ui.support.getMainActivityIntent\nimport org.json.JSONObject\n\n@Suppress(\"LargeClass\", \"TooManyFunctions\")\nobject BrazeNotificationUtils {\n    private enum class BrazeNotificationBroadcastType(val brazePushEventType: BrazePushEventType) {\n        OPENED(NOTIFICATION_OPENED),\n        RECEIVED(NOTIFICATION_RECEIVED),\n        DELETED(NOTIFICATION_DELETED)\n    }\n\n    private val TAG = getBrazeLogTag(BrazeNotificationUtils::class.java)\n    private const val SOURCE_KEY = \"source\"\n\n    /**\n     * Returns a custom [IBrazeNotificationFactory] if set, else the default [IBrazeNotificationFactory].\n     */\n    @get:JvmStatic\n    val activeNotificationFactory: IBrazeNotificationFactory\n        get() {\n            return Braze.customBrazeNotificationFactory ?: BrazeNotificationFactory.instance\n        }\n\n    /**\n     * The [Class] of the notification receiver used by this application.\n     */\n    @get:JvmStatic\n    val notificationReceiverClass: Class<*>\n        get() = if (isAmazonDevice) {\n            BrazeAmazonDeviceMessagingReceiver::class.java\n        } else {\n            BrazePushReceiver::class.java\n        }\n\n    /**\n     * Handles a push notification click. Called by [BrazePushReceiver] when a\n     * Braze push notification click intent is received.\n     *\n     * See [sendNotificationOpenedBroadcast]\n     *\n     * @param context Application context\n     * @param intent  the internal notification clicked intent constructed in\n     * [setContentIntentIfPresent]\n     */\n    @JvmStatic\n    fun handleNotificationOpened(context: Context, intent: Intent) {\n        try {\n            Braze.getInstance(context).logPushNotificationOpened(intent)\n            sendNotificationOpenedBroadcast(context, intent)\n            val appConfigurationProvider = BrazeConfigurationProvider(context)\n            if (appConfigurationProvider.doesHandlePushDeepLinksAutomatically) {\n                routeUserWithNotificationOpenedIntent(context, intent)\n            } else {\n                brazelog(I) { \"Not handling deep links automatically, skipping deep link handling\" }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Exception occurred attempting to handle notification opened intent.\" }\n        }\n    }\n\n    /**\n     * Handles a push notification deletion by the user. Called by [BrazePushReceiver] receiver when a\n     * Braze push notification delete intent is received.\n     *\n     * @see [NotificationCompat.Builder.setDeleteIntent]\n     * @param context Application context\n     * @param intent  the internal notification delete intent constructed in\n     * [setDeleteIntent]\n     */\n    @JvmStatic\n    fun handleNotificationDeleted(context: Context, intent: Intent) {\n        try {\n            brazelog { \"Sending notification deleted broadcast\" }\n            val notificationExtras = intent.extras\n            if (notificationExtras != null) {\n                val notificationPayload = BrazeNotificationPayload(notificationExtras, context = context)\n                sendPushActionIntent(context, BrazeNotificationBroadcastType.DELETED, notificationExtras, notificationPayload)\n            } else {\n                sendPushActionIntent(context, BrazeNotificationBroadcastType.DELETED, notificationExtras)\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Exception occurred attempting to handle notification delete intent.\" }\n        }\n    }\n\n    /**\n     * Opens any available deep links with an Intent.ACTION_VIEW intent, placing the main activity\n     * on the back stack. If no deep link is available, opens the main activity.\n     *\n     * @param context\n     * @param intent  the internal notification clicked intent constructed in\n     * [setContentIntentIfPresent]\n     */\n    @JvmStatic\n    fun routeUserWithNotificationOpenedIntent(context: Context, intent: Intent) {\n        // get extras bundle.\n        var extras = intent.getBundleExtra(Constants.BRAZE_PUSH_EXTRAS_KEY)\n        if (extras == null) {\n            extras = Bundle()\n        }\n        extras.putString(\n            Constants.BRAZE_PUSH_CAMPAIGN_ID_KEY,\n            intent.getStringExtra(Constants.BRAZE_PUSH_CAMPAIGN_ID_KEY)\n        )\n        extras.putString(SOURCE_KEY, Constants.BRAZE)\n\n        // If a deep link exists, start an ACTION_VIEW intent pointing at the deep link.\n        // The intent returned from getStartActivityIntent() is placed on the back stack.\n        // Otherwise, start the intent defined in getStartActivityIntent().\n        val deepLink = intent.getStringExtra(Constants.BRAZE_PUSH_DEEP_LINK_KEY)\n        if (!deepLink.isNullOrBlank()) {\n            val useWebView = \"true\".equals(intent.getStringExtra(Constants.BRAZE_PUSH_OPEN_URI_IN_WEBVIEW_KEY), ignoreCase = true)\n            brazelog { \"Found a deep link: $deepLink. Use webview set to: $useWebView\" }\n\n            // Pass deeplink and use webview values to target activity.\n            extras.putString(Constants.BRAZE_PUSH_DEEP_LINK_KEY, deepLink)\n            extras.putBoolean(Constants.BRAZE_PUSH_OPEN_URI_IN_WEBVIEW_KEY, useWebView)\n            getInstance().createUriActionFromUrlString(deepLink, extras, useWebView, Channel.PUSH)?.let {\n                getInstance().gotoUri(context, it)\n            }\n        } else {\n            val mainActivityIntent = getMainActivityIntent(context, extras)\n            brazelog { \"Push notification had no deep link. Opening main activity: $mainActivityIntent\" }\n            context.startActivity(mainActivityIntent)\n        }\n    }\n\n    /**\n     * Checks the incoming notification intent to determine whether it is a Braze push message.\n     *\n     * All Braze push messages must contain an extras entry with key set to [Constants.BRAZE_PUSH_BRAZE_KEY] and value set to \"true\".\n     */\n    @JvmStatic\n    fun Intent.isBrazePushMessage(): Boolean {\n        val extras = this.extras ?: return false\n        return \"true\".equals(extras.getString(Constants.BRAZE_PUSH_BRAZE_KEY), ignoreCase = true)\n    }\n\n    /**\n     * Checks the notification intent to determine whether this is a notification message or a\n     * silent push.\n     *\n     * A notification message is a Braze push message that displays a notification in the\n     * notification center (and optionally contains extra information that can be used directly\n     * by the app).\n     *\n     * A silent push is a Braze push message that contains only extra information that can\n     * be used directly by the app.\n     */\n    @JvmStatic\n    fun isNotificationMessage(intent: Intent): Boolean {\n        val extras = intent.extras ?: return false\n        return extras.containsKey(Constants.BRAZE_PUSH_TITLE_KEY) && extras.containsKey(Constants.BRAZE_PUSH_CONTENT_KEY)\n    }\n\n    /**\n     * Creates and sends a broadcast message that can be listened for by the host app. The broadcast\n     * message intent contains all of the data sent as part of the Braze push message.\n     */\n    @JvmStatic\n    fun sendPushMessageReceivedBroadcast(\n        context: Context,\n        notificationExtras: Bundle,\n        payload: BrazeNotificationPayload\n    ) {\n        brazelog { \"Sending push message received broadcast\" }\n        sendPushActionIntent(context, BrazeNotificationBroadcastType.RECEIVED, notificationExtras, payload)\n    }\n\n    /**\n     * Requests a geofence refresh from Braze if appropriate based on the payload of the push notification.\n     *\n     * @return True iff a geofence refresh was requested from Braze.\n     */\n    @JvmStatic\n    fun requestGeofenceRefreshIfAppropriate(payload: BrazeNotificationPayload): Boolean {\n        val context = payload.context\n        return if (payload.shouldSyncGeofences && context != null) {\n            brazelog { \"Geofence sync key was true. Syncing geofences.\" }\n            requestGeofenceRefresh(context, true)\n            true\n        } else {\n            brazelog { \"Geofence sync key not included in push payload or false. Not syncing geofences.\" }\n            false\n        }\n    }\n\n    /**\n     * Refreshes a feature flags refresh from Braze if appropriate based on the payload of the push notification.\n     * The SDK will respect the rate limit for feature flag refreshes.\n     *\n     * @return True iff a feature flags refresh was requested from Braze.\n     */\n    @JvmStatic\n    fun refreshFeatureFlagsIfAppropriate(payload: BrazeNotificationPayload): Boolean {\n        val context = payload.context\n        return if (payload.shouldRefreshFeatureFlags && context != null) {\n            brazelog { \"Feature flag refresh key was true. Refreshing feature flags.\" }\n            refreshFeatureFlags(context)\n            true\n        } else {\n            brazelog(V) { \"Feature flag refresh key not included in push payload or false. Not refreshing feature flags.\" }\n            false\n        }\n    }\n\n    /**\n     * Creates an alarm which will issue a broadcast to cancel the notification\n     * specified by the given [notificationId] after the given duration.\n     */\n    @JvmStatic\n    fun setNotificationDurationAlarm(context: Context, thisClass: Class<*>?, notificationId: Int, durationInMillis: Int) {\n        val cancelIntent = Intent(context, thisClass)\n        cancelIntent.action = Constants.BRAZE_CANCEL_NOTIFICATION_ACTION\n        cancelIntent.putExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID, notificationId)\n        val flags = PendingIntent.FLAG_UPDATE_CURRENT or getImmutablePendingIntentFlags()\n        val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, flags)\n        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager\n        if (durationInMillis >= Constants.BRAZE_MINIMUM_NOTIFICATION_DURATION_MILLIS) {\n            brazelog { \"Setting Notification duration alarm for $durationInMillis ms\" }\n            alarmManager[AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + durationInMillis] = pendingIntent\n        }\n    }\n\n    /**\n     * Returns an id for the new notification we'll send to the notification center.\n     * Notification id is used by the Android OS to override currently active notifications with identical ids.\n     * If a custom notification id is not defined in the payload, Braze derives an id value from the message's contents\n     * to prevent duplication in the notification center.\n     */\n    @JvmStatic\n    fun getNotificationId(payload: BrazeNotificationPayload): Int {\n        val customNotificationId = payload.customNotificationId\n        return if (customNotificationId != null) {\n            brazelog { \"Using notification id provided in the message's extras bundle: $customNotificationId\" }\n            customNotificationId\n        } else {\n            var messageKey: String? = \"\"\n            // Don't concatenate if null\n            payload.titleText?.let { messageKey += it }\n            payload.contentText?.let { messageKey += it }\n            val notificationId = messageKey.hashCode()\n            brazelog {\n                \"Message without notification id provided in the extras bundle \" +\n                    \"received. Using a hash of the message: $notificationId\"\n            }\n            notificationId\n        }\n    }\n\n    /**\n     * This method will retrieve notification priority from notificationExtras bundle if it has been set.\n     * Otherwise returns the default priority.\n     *\n     * Starting with Android O, priority is set on a notification channel and not individually on notifications.\n     */\n    @JvmStatic\n    fun getNotificationPriority(payload: BrazeNotificationPayload): Int {\n        val notificationPriority = payload.notificationPriorityInt\n        payload.notificationPriorityInt?.let {\n            @Suppress(\"DEPRECATION\")\n            if (it in Notification.PRIORITY_MIN..Notification.PRIORITY_MAX) {\n                return it\n            } else {\n                brazelog(W) { \"Received invalid notification priority $notificationPriority\" }\n            }\n        }\n        @Suppress(\"DEPRECATION\")\n        return Notification.PRIORITY_DEFAULT\n    }\n\n    @JvmStatic\n    fun wakeScreenIfAppropriate(context: Context, configurationProvider: BrazeConfigurationProvider, notificationExtras: Bundle?): Boolean {\n        return wakeScreenIfAppropriate(\n            BrazeNotificationPayload(\n                notificationExtras = notificationExtras,\n                context = context,\n                configurationProvider = configurationProvider\n            )\n        )\n    }\n\n    /**\n     * This method will wake the device using a wake lock if the [android.Manifest.permission.WAKE_LOCK] permission is present in the\n     * manifest. If the permission is not present, this does nothing. If the screen is already on,\n     * and the permission is present, this does nothing. If the priority of the incoming notification\n     * is min, this does nothing.\n     */\n    @SuppressLint(\"WakelockTimeout\")\n    @Suppress(\"ReturnCount\")\n    @JvmStatic\n    fun wakeScreenIfAppropriate(payload: BrazeNotificationPayload): Boolean {\n        val context = payload.context ?: return false\n        val configurationProvider = payload.configurationProvider ?: return false\n        val notificationExtras = payload.notificationExtras\n\n        // Check for the wake lock permission.\n        if (!context.hasPermission(Manifest.permission.WAKE_LOCK)\n            || !configurationProvider.isPushWakeScreenForNotificationEnabled\n        ) {\n            return false\n        }\n        try {\n            // Never wake a TV panel\n            val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager\n            if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {\n                brazelog { \"Not waking this TV UI mode device\" }\n                return false\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to check for TV status during screen wake. Continuing.\" }\n        }\n\n        // Don't wake lock if this is a minimum priority/importance notification.\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            // Get the channel for this notification\n            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n            val notificationChannel = getValidNotificationChannel(notificationManager, notificationExtras)\n            if (notificationChannel == null) {\n                brazelog { \"Not waking screen on Android O+ device, could not find notification channel.\" }\n                return false\n            }\n            if (notificationChannel.importance == NotificationManager.IMPORTANCE_MIN) {\n                brazelog { \"Not acquiring wake-lock for Android O+ notification with importance: ${notificationChannel.importance}\" }\n                return false\n            }\n        } else {\n            @Suppress(\"DEPRECATION\")\n            if (getNotificationPriority(payload) == Notification.PRIORITY_MIN) {\n                return false\n            }\n        }\n\n        brazelog { \"Waking screen for notification\" }\n        val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager\n        // Deprecation warning suppressed for PowerManager.FULL_WAKE_LOCK usage.\n        // Alternative requires Activity instance which is unavailable in this context.\n        @Suppress(\"DEPRECATION\")\n        val wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG)\n        // Acquire the wake lock for some negligible time, then release it. We just want to wake the screen\n        // and not take up more CPU power than necessary.\n        wakeLock.acquire()\n        wakeLock.release()\n        return true\n    }\n\n    /**\n     * Checks that the notification is a story that has only just been received. If so, each\n     * image within the story is put in the Braze image loader's cache.\n     */\n    @JvmStatic\n    fun prefetchBitmapsIfNewlyReceivedStoryPush(payload: BrazeNotificationPayload) {\n        val context = payload.context ?: return\n        if (!payload.isPushStory || !payload.isNewlyReceivedPushStory) return\n\n        payload.pushStoryPages\n            .mapNotNull { it.bitmapUrl }\n            .forEach {\n                brazelog(V) { \"Pre-fetching bitmap at URL: $it\" }\n                Braze.getInstance(context).imageLoader\n                    .getPushBitmapFromUrl(context, payload.brazeExtras, it, BrazeViewBounds.NOTIFICATION_ONE_IMAGE_STORY)\n            }\n        payload.isNewlyReceivedPushStory = false\n    }\n\n    @JvmStatic\n    fun setTitleIfPresent(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        brazelog { \"Setting title for notification\" }\n        val titleText = payload.titleText ?: return\n        val configurationProvider = payload.configurationProvider ?: return\n        notificationBuilder.setContentTitle(titleText.getHtmlSpannedTextIfEnabled(configurationProvider))\n    }\n\n    /**\n     * Sets notification content if it exists in the payload.\n     */\n    @JvmStatic\n    fun setContentIfPresent(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        brazelog { \"Setting content for notification\" }\n        val contentText = payload.contentText ?: return\n        val configurationProvider = payload.configurationProvider ?: return\n        notificationBuilder.setContentText(contentText.getHtmlSpannedTextIfEnabled(configurationProvider))\n    }\n\n    /**\n     * Sets notification ticker to the title if it exists in the payload.\n     */\n    @JvmStatic\n    fun setTickerIfPresent(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        brazelog { \"Setting ticker for notification\" }\n        val titleText = payload.titleText ?: return\n        notificationBuilder.setTicker(titleText)\n    }\n\n    /**\n     * Create broadcast intent that will fire when the notification has been opened. [BrazePushReceiver] will be notified,\n     * log a click, then send a broadcast to the client receiver.\n     */\n    @JvmStatic\n    fun setContentIntentIfPresent(context: Context, notificationBuilder: NotificationCompat.Builder, notificationExtras: Bundle?) {\n        try {\n            val pushOpenedPendingIntent = getPushActionPendingIntent(context, Constants.BRAZE_PUSH_CLICKED_ACTION, notificationExtras)\n            notificationBuilder.setContentIntent(pushOpenedPendingIntent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Error setting content intent.\" }\n        }\n    }\n\n    @JvmStatic\n    fun setDeleteIntent(context: Context, notificationBuilder: NotificationCompat.Builder, notificationExtras: Bundle?) {\n        try {\n            val pushDeletedIntent = Intent(Constants.BRAZE_PUSH_DELETED_ACTION).setClass(context, notificationReceiverClass)\n            if (notificationExtras != null) {\n                pushDeletedIntent.putExtras(notificationExtras)\n            }\n            val flags = PendingIntent.FLAG_ONE_SHOT or getImmutablePendingIntentFlags()\n            val pushDeletedPendingIntent = PendingIntent.getBroadcast(context, getRequestCode(), pushDeletedIntent, flags)\n            notificationBuilder.setDeleteIntent(pushDeletedPendingIntent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Error setting delete intent.\" }\n        }\n    }\n\n    /**\n     * Sets the icon used in the notification bar itself.\n     * If a drawable defined in braze.xml is found, we use that. Otherwise, fall back to the application icon.\n     *\n     * @return the resource id of the small icon to be used.\n     */\n    @JvmStatic\n    fun setSmallIcon(appConfigurationProvider: BrazeConfigurationProvider, notificationBuilder: NotificationCompat.Builder): Int {\n        var smallNotificationIconResourceId = appConfigurationProvider.smallNotificationIconResourceId\n        if (smallNotificationIconResourceId == 0) {\n            brazelog {\n                \"Small notification icon resource was not found. \" +\n                    \"Will use the app icon when displaying notifications.\"\n            }\n            smallNotificationIconResourceId = appConfigurationProvider.applicationIconResourceId\n        } else {\n            brazelog { \"Setting small icon for notification via resource id\" }\n        }\n        notificationBuilder.setSmallIcon(smallNotificationIconResourceId)\n        return smallNotificationIconResourceId\n    }\n\n    /**\n     * This method exists to disable [NotificationCompat.Builder.setShowWhen]\n     * for push stories.\n     */\n    @JvmStatic\n    fun setSetShowWhen(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        if (payload.isPushStory) {\n            brazelog { \"Set show when not supported in story push.\" }\n            notificationBuilder.setShowWhen(false)\n        }\n    }\n\n    /**\n     * Set large icon. We use the large icon URL if it exists in the notificationExtras.\n     * Otherwise we search for a drawable defined in braze.xml. If that doesn't exists, we do nothing.\n     *\n     * @return whether a large icon was successfully set.\n     */\n    @Suppress(\"ReturnCount\")\n    @JvmStatic\n    fun setLargeIconIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload): Boolean {\n        if (payload.isPushStory) {\n            brazelog { \"Large icon not supported in story push.\" }\n            return false\n        }\n        val context = payload.context ?: return false\n        val appConfigurationProvider = payload.configurationProvider ?: return false\n\n        try {\n            brazelog { \"Setting large icon for notification\" }\n            payload.largeIcon?.let {\n                val largeNotificationBitmap = Braze.getInstance(context)\n                    .imageLoader\n                    .getPushBitmapFromUrl(\n                        context,\n                        extras = null,\n                        imageUrl = it,\n                        BrazeViewBounds.NOTIFICATION_LARGE_ICON\n                    )\n                notificationBuilder.setLargeIcon(largeNotificationBitmap)\n                return true\n            }\n\n            brazelog { \"Large icon bitmap url not present in extras. Attempting to use resource id instead.\" }\n            val largeNotificationIconResourceId = appConfigurationProvider.largeNotificationIconResourceId\n            if (largeNotificationIconResourceId != 0) {\n                val largeNotificationBitmap = BitmapFactory.decodeResource(context.resources, largeNotificationIconResourceId)\n                notificationBuilder.setLargeIcon(largeNotificationBitmap)\n                return true\n            } else {\n                brazelog { \"Large icon resource id not present for notification\" }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Error setting large notification icon\" }\n        }\n        brazelog { \"Large icon not set for notification\" }\n        return false\n    }\n\n    /**\n     * Notifications can optionally include a sound to play when the notification is delivered.\n     *\n     * Starting with Android O, sound is set on a notification channel and not individually on notifications.\n     */\n    @JvmStatic\n    fun setSoundIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        val soundUri = payload.notificationSound ?: return\n        if (soundUri == Constants.BRAZE_PUSH_NOTIFICATION_SOUND_DEFAULT_VALUE) {\n            brazelog { \"Setting default sound for notification.\" }\n            notificationBuilder.setDefaults(Notification.DEFAULT_SOUND)\n        } else {\n            brazelog { \"Setting sound for notification via uri.\" }\n            notificationBuilder.setSound(Uri.parse(soundUri))\n        }\n    }\n\n    /**\n     * Sets the subText of the notification if a summary is present in the notification extras.\n     */\n    @JvmStatic\n    fun setSummaryTextIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        val summaryText = payload.summaryText\n        if (summaryText != null) {\n            brazelog { \"Setting summary text for notification\" }\n            notificationBuilder.setSubText(summaryText)\n        } else {\n            brazelog { \"Summary text not present. Not setting summary text for notification.\" }\n        }\n    }\n\n    /**\n     * Sets the priority of the notification if a priority is present in the notification extras.\n     *\n     * Starting with Android O, priority is set on a notification channel and not individually on notifications.\n     */\n    @JvmStatic\n    fun setPriorityIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        brazelog { \"Setting priority for notification\" }\n        notificationBuilder.priority = getNotificationPriority(payload)\n    }\n\n    /**\n     * Set accent color for devices on Lollipop and above. We use the push-specific accent color if it exists in the notificationExtras,\n     * otherwise we search for a default set in braze.xml or don't set the color at all (and the system notification gray\n     * default is used).\n     *\n     * Supported Lollipop+.\n     */\n    @JvmStatic\n    fun setAccentColorIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n            return\n        }\n        val accentColor = payload.accentColor\n        if (accentColor != null) {\n            brazelog { \"Using accent color for notification from extras bundle\" }\n            notificationBuilder.color = accentColor\n        } else {\n            payload.configurationProvider?.let {\n                brazelog { \"Using default accent color for notification\" }\n                notificationBuilder.color = it.defaultNotificationAccentColor\n            }\n        }\n    }\n\n    /**\n     * Set category for devices on Lollipop and above. Category is one of the predefined notification\n     * categories (see the CATEGORY_* constants in Notification)\n     * that best describes a Notification. May be used by the system for ranking and filtering.\n     *\n     * Supported Lollipop+.\n     */\n    @JvmStatic\n    fun setCategoryIfPresentAndSupported(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n            brazelog {\n                \"Notification category not supported on this \" +\n                    \"android version. Not setting category for notification.\"\n            }\n            return\n        }\n        val notificationCategory = payload.notificationCategory\n        if (notificationCategory != null) {\n            brazelog { \"Setting category for notification\" }\n            notificationBuilder.setCategory(notificationCategory)\n        } else {\n            brazelog { \"Category not present in notification extras. Not setting category for notification.\" }\n        }\n    }\n\n    /**\n     * Set visibility for devices on Lollipop and above.\n     *\n     * Sphere of visibility of this notification, which affects how and when the SystemUI reveals the notification's presence and\n     * contents in untrusted situations (namely, on the secure lockscreen). The default level, VISIBILITY_PRIVATE, behaves exactly\n     * as notifications have always done on Android: The notification's icon and tickerText (if available) are shown in all situations,\n     * but the contents are only available if the device is unlocked for the appropriate user. A more permissive policy can be expressed\n     * by VISIBILITY_PUBLIC; such a notification can be read even in an \"insecure\" context (that is, above a secure lockscreen).\n     * To modify the public version of this notification—for example, to redact some portions—see setPublicVersion(Notification).\n     * Finally, a notification can be made VISIBILITY_SECRET, which will suppress its icon and ticker until the user has bypassed the lockscreen.\n     *\n     * Supported Lollipop+.\n     */\n    @JvmStatic\n    fun setVisibilityIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n            brazelog {\n                \"Notification visibility not supported on \" +\n                    \"this android version. Not setting visibility for notification.\"\n            }\n            return\n        }\n        val visibility = payload.notificationVisibility\n        if (visibility != null) {\n            if (isValidNotificationVisibility(visibility)) {\n                brazelog { \"Setting visibility for notification\" }\n                notificationBuilder.setVisibility(visibility)\n            } else {\n                brazelog(W) { \"Received invalid notification visibility $visibility\" }\n            }\n        }\n    }\n\n    /**\n     * Set the public version of the notification for notifications with private visibility.\n     *\n     * Supported Lollipop+.\n     */\n    @JvmStatic\n    fun setPublicVersionIfPresentAndSupported(notificationBuilder: NotificationCompat.Builder, payload: BrazeNotificationPayload) {\n        val context = payload.context\n        val appConfigurationProvider = payload.configurationProvider\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n            brazelog { \"Cannot set public version before Lollipop\" }\n            return\n        }\n        if (context == null\n            || payload.publicNotificationExtras == null\n            || appConfigurationProvider == null\n        ) {\n            return\n        }\n        val notificationChannelId = getOrCreateNotificationChannelId(payload)\n        val publicNotificationExtras = payload.publicNotificationExtras.parseJsonObjectIntoBundle()\n        if (publicNotificationExtras.isEmpty) return\n\n        val publicPayload = BrazeNotificationPayload(\n            notificationExtras = publicNotificationExtras,\n            context = context,\n            configurationProvider = appConfigurationProvider\n        )\n        val publicNotificationBuilder = NotificationCompat.Builder(context, notificationChannelId)\n\n        brazelog { \"Setting public version of notification with payload: $publicPayload\" }\n        setContentIfPresent(publicNotificationBuilder, publicPayload)\n        setTitleIfPresent(publicNotificationBuilder, publicPayload)\n        setSummaryTextIfPresentAndSupported(publicNotificationBuilder, publicPayload)\n        setSmallIcon(appConfigurationProvider, publicNotificationBuilder)\n        setAccentColorIfPresentAndSupported(publicNotificationBuilder, publicPayload)\n        notificationBuilder.setPublicVersion(publicNotificationBuilder.build())\n    }\n\n    /**\n     * Checks whether the given integer value is a valid Android notification visibility constant.\n     */\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    @JvmStatic\n    fun isValidNotificationVisibility(visibility: Int): Boolean =\n        visibility == Notification.VISIBILITY_SECRET || visibility == Notification.VISIBILITY_PRIVATE || visibility == Notification.VISIBILITY_PUBLIC\n\n    /**\n     * Logs a notification click with Braze if the extras passed down\n     * indicate that they are from Braze and contain a campaign Id.\n     *\n     * A Braze session must be active to log a push notification.\n     *\n     * @param customContentString extra key value pairs in JSON format.\n     */\n    @JvmStatic\n    fun logBaiduNotificationClick(context: Context?, customContentString: String?) {\n        if (customContentString == null) {\n            brazelog(W) { \"customContentString was null. Doing nothing.\" }\n            return\n        }\n        if (context == null) {\n            brazelog(W) { \"Cannot log baidu click with null context. Doing nothing.\" }\n            return\n        }\n        try {\n            val jsonExtras = JSONObject(customContentString)\n            val source = jsonExtras.getOptionalString(SOURCE_KEY)\n            val campaignId = jsonExtras.getOptionalString(Constants.BRAZE_PUSH_CAMPAIGN_ID_KEY)\n            if (source != null && source == Constants.BRAZE && campaignId != null) {\n                Braze.getInstance(context).logPushNotificationOpened(campaignId)\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Caught an exception processing customContentString: $customContentString\" }\n        }\n    }\n\n    /**\n     * Handles a request to cancel a push notification in the notification center. Called\n     * by [BrazePushReceiver] when a Braze cancel notification intent is received.\n     *\n     * Any existing notification in the notification center with the integer Id specified in the\n     * \"nid\" field of the provided intent's extras is cancelled.\n     *\n     * If no Id is found, the default Braze notification Id is used.\n     *\n     * @param context\n     * @param intent  the cancel notification intent\n     */\n    @JvmStatic\n    fun handleCancelNotificationAction(context: Context, intent: Intent) {\n        try {\n            if (intent.hasExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID)) {\n                val notificationId = intent.getIntExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID, Constants.BRAZE_DEFAULT_NOTIFICATION_ID)\n                brazelog { \"Cancelling notification action with id: $notificationId\" }\n                val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n                notificationManager.cancel(Constants.BRAZE_PUSH_NOTIFICATION_TAG, notificationId)\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Exception occurred handling cancel notification intent.\" }\n        }\n    }\n\n    /**\n     * Creates a request to cancel a push notification in the notification center.\n     *\n     * Sends an intent to the [BrazePushReceiver] requesting Braze to cancel the notification with\n     * the specified notification Id.\n     *\n     * See [handleCancelNotificationAction]\n     */\n    @JvmStatic\n    fun cancelNotification(context: Context, notificationId: Int) {\n        try {\n            brazelog { \"Cancelling notification action with id: $notificationId\" }\n            val cancelNotificationIntent = Intent(Constants.BRAZE_CANCEL_NOTIFICATION_ACTION).setClass(context, notificationReceiverClass)\n            cancelNotificationIntent.setPackage(context.packageName)\n            cancelNotificationIntent.putExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID, notificationId)\n            addComponentAndSendBroadcast(context, cancelNotificationIntent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Exception occurred attempting to cancel notification.\" }\n        }\n    }\n\n    /**\n     * Returns true if the bundle is from a push sent by\n     * Braze for uninstall tracking. Uninstall tracking push can be ignored.\n     *\n     * @param notificationExtras A notificationExtras bundle that is passed\n     * with the push received intent when a notification message is\n     * received, and that Braze passes in the intent to registered receivers.\n     */\n    @JvmStatic\n    @Deprecated(\"Please use BrazeNotificationPayload().isUninstallTracking instead\")\n    fun isUninstallTrackingPush(notificationExtras: Bundle): Boolean {\n        try {\n            // The ADM case where extras are flattened\n            if (notificationExtras.containsKey(Constants.BRAZE_PUSH_UNINSTALL_TRACKING_KEY)) {\n                return true\n            }\n            // The FCM case where extras are in a separate bundle\n            val fcmExtras = notificationExtras.getBundle(Constants.BRAZE_PUSH_EXTRAS_KEY)\n            if (fcmExtras != null) {\n                return fcmExtras.containsKey(Constants.BRAZE_PUSH_UNINSTALL_TRACKING_KEY)\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to determine if push is uninstall tracking. Returning false.\" }\n        }\n        return false\n    }\n\n    /**\n     * Returns the channel id for a valid [NotificationChannel], creating one if necessary.\n     *\n     * First, if [Constants.BRAZE_PUSH_NOTIFICATION_CHANNEL_ID_KEY] key is present in\n     * notificationExtras's and is the id of a valid NotificationChannel, this id will\n     * be returned.\n     *\n     * Next, if the channel with id [Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID] exists,\n     * then [Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID] will be returned.\n     *\n     * Finally, if neither of the cases above is true, a channel with id [Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID]\n     * will be created and [Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID] will be\n     * returned.\n     */\n    @JvmStatic\n    fun getOrCreateNotificationChannelId(payload: BrazeNotificationPayload): String {\n        val channelIdFromExtras = payload.notificationChannelId\n        val defaultChannelId = Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n            // If on Android < O, the channel does not really need to exist\n            return channelIdFromExtras ?: defaultChannelId\n        }\n        val context = payload.context\n        val config = payload.configurationProvider\n        val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n        // First try to get the channel from the extras\n        if (channelIdFromExtras != null) {\n            if (notificationManager.getNotificationChannel(channelIdFromExtras) != null) {\n                brazelog { \"Found notification channel in extras with id: $channelIdFromExtras\" }\n                return channelIdFromExtras\n            } else {\n                brazelog { \"Notification channel from extras is invalid. No channel found with id: $channelIdFromExtras\" }\n            }\n        }\n\n        // If we get here, we need to use the default channel\n        if (notificationManager.getNotificationChannel(defaultChannelId) == null) {\n            // If the default doesn't exist, create it now\n            brazelog { \"Braze default notification channel does not exist on device. Creating default channel.\" }\n            val channel = NotificationChannel(\n                defaultChannelId,\n                config?.defaultNotificationChannelName,\n                NotificationManager.IMPORTANCE_DEFAULT\n            )\n            channel.description = config?.defaultNotificationChannelDescription\n            notificationManager.createNotificationChannel(channel)\n        }\n        return defaultChannelId\n    }\n\n    /**\n     * Sets the notification number, set via [NotificationCompat.Builder.setNumber].\n     * On Android O, this number is used with notification badges.\n     */\n    @JvmStatic\n    fun setNotificationBadgeNumberIfPresent(\n        notificationBuilder: NotificationCompat.Builder,\n        payload: BrazeNotificationPayload\n    ) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n            brazelog {\n                \"Notification badge number not supported on this \" +\n                    \"android version. Not setting badge number for notification.\"\n            }\n            return\n        }\n        val notificationBadgeNumber = payload.notificationBadgeNumber\n        if (notificationBadgeNumber != null) {\n            notificationBuilder.setNumber(notificationBadgeNumber)\n        }\n    }\n\n    /**\n     * Handles a push story page click. Called by [BrazePushReceiver] when an\n     * Braze push story click intent is received.\n     *\n     * @param context Application context.\n     * @param intent  The push story click intent.\n     */\n    @JvmStatic\n    fun handlePushStoryPageClicked(context: Context, intent: Intent) {\n        try {\n            Braze.getInstance(context)\n                .logPushStoryPageClicked(\n                    intent.getStringExtra(Constants.BRAZE_CAMPAIGN_ID),\n                    intent.getStringExtra(Constants.BRAZE_STORY_PAGE_ID)\n                )\n\n            val appConfigurationProvider = BrazeConfigurationProvider(context)\n\n            val notificationId = intent.getIntExtra(Constants.BRAZE_PUSH_NOTIFICATION_ID, 0)\n            if (appConfigurationProvider.doesPushStoryDismissOnClick && notificationId != 0) {\n                cancelNotification(context, notificationId)\n            }\n\n            val deepLink = intent.getStringExtra(Constants.BRAZE_ACTION_URI_KEY)\n            if (!deepLink.isNullOrBlank()) {\n                // Set the global deep link value to the correct action's deep link.\n                intent.putExtra(Constants.BRAZE_PUSH_DEEP_LINK_KEY, intent.getStringExtra(Constants.BRAZE_ACTION_URI_KEY))\n                val useWebviewString = intent.getStringExtra(Constants.BRAZE_ACTION_USE_WEBVIEW_KEY)\n                if (!useWebviewString.isNullOrBlank()) {\n                    intent.putExtra(Constants.BRAZE_PUSH_OPEN_URI_IN_WEBVIEW_KEY, useWebviewString)\n                }\n            } else {\n                // Otherwise, remove any existing deep links.\n                intent.removeExtra(Constants.BRAZE_PUSH_DEEP_LINK_KEY)\n            }\n            sendNotificationOpenedBroadcast(context, intent)\n\n            if (appConfigurationProvider.doesHandlePushDeepLinksAutomatically) {\n                routeUserWithNotificationOpenedIntent(context, intent)\n            } else {\n                brazelog(I) { \"Not handling deep links automatically, skipping deep link handling for '$deepLink'\" }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Caught exception while handling story click.\" }\n        }\n    }\n\n    /**\n     * Parses the notification bundle for any associated\n     * ContentCards, if present. If found, the card object\n     * is added to card storage.\n     */\n    @JvmStatic\n    fun handleContentCardsSerializedCardIfPresent(payload: BrazeNotificationPayload) {\n        val contentCardData = payload.contentCardSyncData\n        val contentCardDataUserId = payload.contentCardSyncUserId\n        val context = payload.context\n        if (contentCardData != null && context != null) {\n            brazelog { \"Push contains associated Content Cards card. User id: $contentCardDataUserId Card data: $contentCardData\" }\n            addSerializedContentCardToStorage(context, contentCardData, contentCardDataUserId)\n        }\n    }\n\n    /**\n     * Sends a push notification opened broadcast to the client broadcast receiver.\n     *\n     * @param context Application context\n     * @param intent  The internal notification clicked intent constructed in\n     * [setContentIntentIfPresent]\n     */\n    @JvmStatic\n    fun sendNotificationOpenedBroadcast(context: Context, intent: Intent) {\n        brazelog { \"Sending notification opened broadcast\" }\n        val notificationExtras = intent.extras\n        if (notificationExtras != null) {\n            val notificationPayload = BrazeNotificationPayload(notificationExtras, context = context)\n            sendPushActionIntent(context, BrazeNotificationBroadcastType.OPENED, notificationExtras, notificationPayload)\n        } else {\n            sendPushActionIntent(context, BrazeNotificationBroadcastType.OPENED, notificationExtras)\n        }\n    }\n\n    /**\n     * Returns an existing notification channel. The notification extras are first checked for\n     * a notification channel that exists. If not, then the default\n     * Braze notification channel is returned if it exists. If neither\n     * exist on the device, then null is returned.\n     *\n     * This method does not create a notification channel if a valid channel cannot be found.\n     *\n     * @param notificationExtras The extras that will be checked for a valid notification channel id.\n     * @return A already created notification channel on the device, or null if one cannot be found.\n     */\n    @TargetApi(Build.VERSION_CODES.O)\n    @JvmStatic\n    fun getValidNotificationChannel(notificationManager: NotificationManager, notificationExtras: Bundle?): NotificationChannel? {\n        if (notificationExtras == null) {\n            brazelog { \"Notification extras bundle was null. Could not find a valid notification channel\" }\n            return null\n        }\n        val channelIdFromExtras = notificationExtras.getString(Constants.BRAZE_PUSH_NOTIFICATION_CHANNEL_ID_KEY, null)\n        if (!channelIdFromExtras.isNullOrBlank()) {\n            val notificationChannel = notificationManager.getNotificationChannel(channelIdFromExtras)\n            if (notificationChannel != null) {\n                brazelog { \"Found notification channel in extras with id: $channelIdFromExtras\" }\n                return notificationChannel\n            } else {\n                brazelog { \"Notification channel from extras is invalid, no channel found with id: $channelIdFromExtras\" }\n            }\n        }\n        val defaultNotificationChannel = notificationManager.getNotificationChannel(Constants.BRAZE_PUSH_DEFAULT_NOTIFICATION_CHANNEL_ID)\n        if (defaultNotificationChannel != null) {\n            return defaultNotificationChannel\n        } else {\n            brazelog { \"Braze default notification channel does not exist on device.\" }\n        }\n        return null\n    }\n\n    /**\n     * Creates a [PendingIntent] using the given action and extras specified.\n     *\n     * @param context            Application context\n     * @param action             The action to set for the [PendingIntent]\n     * @param notificationExtras The extras to set for the [PendingIntent], if not null\n     */\n    private fun getPushActionPendingIntent(\n        context: Context,\n        @Suppress(\"SameParameterValue\") action: String,\n        notificationExtras: Bundle?\n    ): PendingIntent {\n        val pushActionIntent = Intent(action).setClass(context, NotificationTrampolineActivity::class.java)\n        if (notificationExtras != null) {\n            pushActionIntent.putExtras(notificationExtras)\n        }\n        val flags = PendingIntent.FLAG_ONE_SHOT or getImmutablePendingIntentFlags()\n        return PendingIntent.getActivity(context, getRequestCode(), pushActionIntent, flags)\n    }\n\n    /**\n     * Broadcasts an intent with the given action suffix. Will copy the extras from the input intent.\n     *\n     * @param context            Application context.\n     * @param notificationExtras The extras to attach to the intent.\n     * @param payload The notification payload, may be null. If present, will lead to a callback firing for [IBraze.subscribeToPushNotificationEvents]\n     */\n    private fun sendPushActionIntent(\n        context: Context,\n        broadcastType: BrazeNotificationBroadcastType,\n        notificationExtras: Bundle?,\n        payload: BrazeNotificationPayload? = null\n    ) {\n        // This is the current intent whose action does\n        // not require a prefix of the app package name\n        val brazePushIntent: Intent = when (broadcastType) {\n            BrazeNotificationBroadcastType.OPENED -> {\n                Intent(Constants.BRAZE_PUSH_INTENT_NOTIFICATION_OPENED).setPackage(context.packageName)\n            }\n            BrazeNotificationBroadcastType.RECEIVED -> {\n                Intent(Constants.BRAZE_PUSH_INTENT_NOTIFICATION_RECEIVED).setPackage(context.packageName)\n            }\n            BrazeNotificationBroadcastType.DELETED -> {\n                Intent(Constants.BRAZE_PUSH_INTENT_NOTIFICATION_DELETED).setPackage(context.packageName)\n            }\n        }\n        brazelog(V) { \"Sending Braze broadcast receiver intent for $broadcastType\" }\n        sendPushActionIntent(context, brazePushIntent, notificationExtras)\n\n        if (payload != null) {\n            // Send this event to the SDK for publishing\n            BrazeInternal.publishBrazePushAction(context, broadcastType.brazePushEventType, payload)\n        }\n    }\n\n    private fun sendPushActionIntent(context: Context, pushIntent: Intent, notificationExtras: Bundle?) {\n        brazelog(V) { \"Sending push action intent: $pushIntent\" }\n        if (notificationExtras != null) {\n            pushIntent.putExtras(notificationExtras)\n        }\n        addComponentAndSendBroadcast(context, pushIntent)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/BrazePushReceiver.kt",
    "content": "package com.braze.push\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.annotation.VisibleForTesting\nimport androidx.core.app.NotificationManagerCompat\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.models.push.BrazeNotificationPayload.Companion.getAttachedBrazeExtras\nimport com.braze.Braze\nimport com.braze.BrazeInternal.applyPendingRuntimeConfiguration\nimport com.braze.BrazeInternal.handleInAppMessageTestPush\nimport com.braze.Constants\nimport com.braze.Constants.isAmazonDevice\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.push.BrazeNotificationActionUtils.handleNotificationActionClicked\nimport com.braze.push.BrazeNotificationUtils.activeNotificationFactory\nimport com.braze.push.BrazeNotificationUtils.getNotificationId\nimport com.braze.push.BrazeNotificationUtils.handleCancelNotificationAction\nimport com.braze.push.BrazeNotificationUtils.handleNotificationDeleted\nimport com.braze.push.BrazeNotificationUtils.handleNotificationOpened\nimport com.braze.push.BrazeNotificationUtils.handlePushStoryPageClicked\nimport com.braze.push.BrazeNotificationUtils.isBrazePushMessage\nimport com.braze.push.BrazeNotificationUtils.isNotificationMessage\nimport com.braze.push.BrazeNotificationUtils.requestGeofenceRefreshIfAppropriate\nimport com.braze.push.BrazeNotificationUtils.refreshFeatureFlagsIfAppropriate\nimport com.braze.push.BrazeNotificationUtils.sendPushMessageReceivedBroadcast\nimport com.braze.push.BrazeNotificationUtils.setNotificationDurationAlarm\nimport com.braze.push.BrazeNotificationUtils.wakeScreenIfAppropriate\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport kotlinx.coroutines.launch\n\nopen class BrazePushReceiver : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent) {\n        handleReceivedIntent(context, intent)\n    }\n\n    companion object {\n        // ADM keys match FCM for these fields.\n        private const val MESSAGE_TYPE_KEY = \"message_type\"\n        private const val DELETED_MESSAGES_KEY = \"deleted_messages\"\n        private const val NUMBER_OF_MESSAGES_DELETED_KEY = \"total_deleted\"\n        private const val ADM_RECEIVE_INTENT_ACTION = \"com.amazon.device.messaging.intent.RECEIVE\"\n        private const val ADM_REGISTRATION_INTENT_ACTION =\n            \"com.amazon.device.messaging.intent.REGISTRATION\"\n        private const val ADM_ERROR_KEY = \"error\"\n        private const val ADM_ERROR_DESCRIPTION_KEY = \"error_description\"\n        private const val ADM_REGISTRATION_ID_KEY = \"registration_id\"\n        private const val ADM_UNREGISTERED_KEY = \"unregistered\"\n\n        /**\n         * Internal API. Do not use.\n         */\n        const val FIREBASE_MESSAGING_SERVICE_ROUTING_ACTION =\n            \"firebase_messaging_service_routing_action\"\n\n        /**\n         * Internal API. Do not use.\n         */\n        const val HMS_PUSH_SERVICE_ROUTING_ACTION = \"hms_push_service_routing_action\"\n\n        private fun handlePush(\n            context: Context,\n            intent: Intent\n        ) {\n            val applicationContext = context.applicationContext\n            val action: String? = intent.action\n\n            fun performWork() {\n                brazelog(I) { \"Received broadcast message. Message: $intent\" }\n                if (action.isNullOrEmpty()) {\n                    brazelog(W) { \"Push action is null. Not handling intent: $intent\" }\n                    return\n                }\n                applyPendingRuntimeConfiguration(applicationContext)\n                // The Activity context (if provided) should be passed to the methods\n                // fired after the user has clicked the push so that deeplink handling\n                // can work effectively.\n                when (action) {\n                    FIREBASE_MESSAGING_SERVICE_ROUTING_ACTION,\n                    Constants.BRAZE_STORY_TRAVERSE_CLICKED_ACTION,\n                    HMS_PUSH_SERVICE_ROUTING_ACTION,\n                    ADM_RECEIVE_INTENT_ACTION -> handlePushNotificationPayload(\n                        applicationContext,\n                        intent\n                    )\n                    ADM_REGISTRATION_INTENT_ACTION -> handleAdmRegistrationEventIfEnabled(\n                        BrazeConfigurationProvider(applicationContext),\n                        applicationContext,\n                        intent\n                    )\n                    Constants.BRAZE_CANCEL_NOTIFICATION_ACTION -> handleCancelNotificationAction(\n                        applicationContext,\n                        intent\n                    )\n                    Constants.BRAZE_PUSH_DELETED_ACTION -> handleNotificationDeleted(\n                        applicationContext,\n                        intent\n                    )\n\n                    // Methods that later call \"routeUserWithNotificationOpenedIntent\"\n                    // or equivalent and need the Activity context.\n                    Constants.BRAZE_STORY_CLICKED_ACTION -> handlePushStoryPageClicked(\n                        context,\n                        intent\n                    )\n                    Constants.BRAZE_ACTION_CLICKED_ACTION -> handleNotificationActionClicked(\n                        context,\n                        intent\n                    )\n                    Constants.BRAZE_PUSH_CLICKED_ACTION -> handleNotificationOpened(\n                        context,\n                        intent\n                    )\n                    else -> brazelog(W) { \"Received a message not sent from Braze. Ignoring the message.\" }\n                }\n            }\n\n            try {\n                performWork()\n            } catch (e: Exception) {\n                brazelog(E, e) {\n                    \"Caught exception while performing the push notification handling work. \" +\n                        \"Action: $action Intent: $intent\"\n                }\n            }\n        }\n\n        @JvmStatic\n        @JvmOverloads\n        fun handleReceivedIntent(context: Context, intent: Intent, runOnThread: Boolean = true) {\n            if (runOnThread) {\n                // Don't pass an Activity context into a background thread\n                BrazeCoroutineScope.launch {\n                    handlePush(context.applicationContext, intent)\n                }\n            } else {\n                // Run on the caller thread\n                handlePush(context, intent)\n            }\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        fun handleAdmRegistrationEventIfEnabled(\n            appConfigurationProvider: BrazeConfigurationProvider,\n            context: Context,\n            intent: Intent\n        ): Boolean {\n            brazelog(I) { \"Received ADM registration. Message: $intent\" }\n            // Only handle ADM registration events if ADM registration handling is turned on in the\n            // configuration file.\n            if (isAmazonDevice && appConfigurationProvider.isAdmMessagingRegistrationEnabled) {\n                brazelog { \"ADM enabled in braze.xml. Continuing to process ADM registration intent.\" }\n                handleAdmRegistrationIntent(context, intent)\n                return true\n            }\n            brazelog(W) {\n                \"ADM not enabled in braze.xml. Ignoring ADM registration intent. Note: you must set \" +\n                    \"com_braze_push_adm_messaging_registration_enabled to true in your braze.xml to enable ADM.\"\n            }\n            return false\n        }\n\n        /**\n         * Processes the registration/unregistration result returned from the ADM servers. If the\n         * registration/unregistration is successful, this will store/clear the registration ID from the\n         * device. Otherwise, it will log an error message and the device will not be able to receive ADM\n         * messages.\n         */\n        @JvmStatic\n        @VisibleForTesting\n        fun handleAdmRegistrationIntent(context: Context, intent: Intent): Boolean {\n            val error = intent.getStringExtra(ADM_ERROR_KEY)\n            val errorDescription = intent.getStringExtra(ADM_ERROR_DESCRIPTION_KEY)\n            val registrationId = intent.getStringExtra(ADM_REGISTRATION_ID_KEY)\n            val unregistered = intent.getStringExtra(ADM_UNREGISTERED_KEY)\n            when {\n                error != null -> {\n                    brazelog(W) { \"Error during ADM registration: $error description: $errorDescription\" }\n                }\n                registrationId != null -> {\n                    brazelog(I) { \"Registering for ADM messages with registrationId: $registrationId\" }\n                    Braze.getInstance(context).registeredPushToken = registrationId\n                }\n                unregistered != null -> {\n                    brazelog(W) { \"The device was un-registered from ADM: $unregistered\" }\n                }\n                else -> {\n                    brazelog(W) {\n                        \"The ADM registration intent is missing error information, registration id, and unregistration \" +\n                            \"confirmation. Ignoring.\"\n                    }\n                    return false\n                }\n            }\n            return true\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        @Suppress(\"LongMethod\", \"ComplexMethod\", \"ReturnCount\")\n        fun handlePushNotificationPayload(context: Context, intent: Intent): Boolean {\n            when {\n                !intent.isBrazePushMessage() -> {\n                    brazelog { \"Not handling non-Braze push message.\" }\n                    return false\n                }\n                DELETED_MESSAGES_KEY == intent.getStringExtra(MESSAGE_TYPE_KEY) -> {\n                    val totalDeleted = intent.getIntExtra(NUMBER_OF_MESSAGES_DELETED_KEY, -1)\n                    brazelog(I) { \"Firebase messaging '$NUMBER_OF_MESSAGES_DELETED_KEY' reports $totalDeleted messages.\" }\n                    return false\n                }\n            }\n\n            // Since isBrazePushMessage returned true, extras is non-null. This just keeps the compiler happy.\n            val notificationExtras = intent.extras ?: return false\n\n            brazelog(I) { \"Push message payload received: $notificationExtras\" }\n\n            // Convert the JSON in the extras key into a Bundle.\n            val brazeExtras = getAttachedBrazeExtras(notificationExtras)\n            notificationExtras.putBundle(Constants.BRAZE_PUSH_EXTRAS_KEY, brazeExtras)\n            if (!notificationExtras.containsKey(Constants.BRAZE_PUSH_RECEIVED_TIMESTAMP_MILLIS)) {\n                notificationExtras.putLong(\n                    Constants.BRAZE_PUSH_RECEIVED_TIMESTAMP_MILLIS,\n                    System.currentTimeMillis()\n                )\n            }\n\n            val appConfigurationProvider = BrazeConfigurationProvider(context)\n            val payload = createPayload(context, appConfigurationProvider, notificationExtras, brazeExtras)\n\n            if (payload.isUninstallTrackingPush) {\n                // Note that this re-implementation of uninstall tracking\n                // does not forward the notification to receivers.\n                brazelog(I) {\n                    \"Push message is uninstall tracking push. Doing nothing. Not forwarding this \" +\n                        \"notification to broadcast receivers.\"\n                }\n                return false\n            }\n\n            // Parse the notification for any associated ContentCard\n            BrazeNotificationUtils.handleContentCardsSerializedCardIfPresent(payload)\n\n            if (payload.shouldFetchTestTriggers\n                && appConfigurationProvider.isInAppMessageTestPushEagerDisplayEnabled\n                && BrazeInAppMessageManager.getInstance().activity != null\n            ) {\n                // Pass this test in-app message along for\n                // eager display and bypass displaying a push\n                brazelog {\n                    \"Bypassing push display due to test in-app message presence and eager test \" +\n                        \"in-app message display configuration setting.\"\n                }\n                handleInAppMessageTestPush(context, intent)\n                return false\n            }\n\n            if (isNotificationMessage(intent)) {\n                brazelog { \"Received visible push notification\" }\n\n                val notificationId = getNotificationId(payload)\n                notificationExtras.putInt(Constants.BRAZE_PUSH_NOTIFICATION_ID, notificationId)\n                if (payload.isPushStory) {\n                    if (isAmazonDevice) {\n                        brazelog { \"Push stories not supported on Amazon devices.\" }\n                        // In case the backend does send these, handle them gracefully\n                        return false\n                    }\n                    if (!notificationExtras.containsKey(Constants.BRAZE_PUSH_STORY_IS_NEWLY_RECEIVED)) {\n                        brazelog { \"Received the initial Push Story notification.\" }\n                        notificationExtras.putBoolean(\n                            Constants.BRAZE_PUSH_STORY_IS_NEWLY_RECEIVED,\n                            true\n                        )\n                    }\n                }\n\n                brazelog(V) { \"Creating notification with payload:\\n$payload\" }\n                val notification = activeNotificationFactory.createNotification(payload)\n                if (notification == null) {\n                    brazelog { \"Notification created by notification factory was null. Not displaying notification.\" }\n                    return false\n                }\n                val notificationManager = NotificationManagerCompat.from(context)\n                brazelog {\n                    \"Value of notificationManager.areNotificationsEnabled() = ${notificationManager.areNotificationsEnabled()}\"\n                }\n                notificationManager.notify(\n                    Constants.BRAZE_PUSH_NOTIFICATION_TAG,\n                    notificationId,\n                    notification\n                )\n                sendPushMessageReceivedBroadcast(context, notificationExtras, payload)\n                wakeScreenIfAppropriate(context, appConfigurationProvider, notificationExtras)\n\n                // Set a custom duration for this notification.\n                payload.pushDuration?.let { duration ->\n                    setNotificationDurationAlarm(\n                        context,\n                        BrazePushReceiver::class.java,\n                        notificationId,\n                        duration\n                    )\n                }\n                return true\n            } else {\n                brazelog { \"Received silent push notification\" }\n                sendPushMessageReceivedBroadcast(context, notificationExtras, payload)\n                requestGeofenceRefreshIfAppropriate(payload)\n                refreshFeatureFlagsIfAppropriate(payload)\n                return false\n            }\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        fun createPayload(\n            context: Context,\n            appConfigurationProvider: BrazeConfigurationProvider,\n            notificationExtras: Bundle,\n            brazeExtras: Bundle\n        ): BrazeNotificationPayload {\n            // ADM uses a different constructor here because the data is already flattened.\n            return if (isAmazonDevice) {\n                BrazeNotificationPayload(\n                    notificationExtras,\n                    getAttachedBrazeExtras(\n                        notificationExtras\n                    ),\n                    context, appConfigurationProvider\n                )\n            } else {\n                BrazeNotificationPayload(\n                    notificationExtras,\n                    brazeExtras,\n                    context,\n                    appConfigurationProvider\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/NotificationTrampolineActivity.kt",
    "content": "package com.braze.push\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.Constants.isAmazonDevice\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.push.BrazeNotificationUtils.notificationReceiverClass\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\n\nclass NotificationTrampolineActivity : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        brazelog(V) { \"NotificationTrampolineActivity created\" }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        try {\n            val receivedIntent = intent\n            if (receivedIntent == null) {\n                brazelog { \"Notification trampoline activity received null intent. Doing nothing.\" }\n                finish()\n                return\n            }\n            val action = receivedIntent.action\n            if (action == null) {\n                brazelog { \"Notification trampoline activity received intent with null action. Doing nothing.\" }\n                finish()\n                return\n            }\n            brazelog(V) { \"Notification trampoline activity received intent: $receivedIntent\" }\n            // Route the intent back to the receiver\n            val sendIntent = Intent(action).setClass(this, notificationReceiverClass)\n\n            receivedIntent.extras?.let {\n                sendIntent.putExtras(it)\n            }\n            if (isAmazonDevice) {\n                BrazePushReceiver.handleReceivedIntent(this, sendIntent)\n            } else {\n                BrazePushReceiver.handleReceivedIntent(this, sendIntent, false)\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to route intent to notification receiver\" }\n        }\n\n        // Now that this Activity has been created, we are safe to finish it in accordance with\n        // https://developer.android.com/guide/components/activities/background-starts#exceptions\n        // Guarantee that this Activity gets finished if onPause() is never called\n\n        brazelog(V) { \"Notification trampoline activity finished processing. Delaying before finishing activity.\" }\n        @Suppress(\"MagicNumber\")\n        BrazeCoroutineScope.launchDelayed(200) {\n            brazelog(V) { \"Delay complete. Finishing Notification trampoline activity now\" }\n            finish()\n        }\n    }\n\n    override fun onPause() {\n        super.onPause()\n        brazelog(V) { \"Notification trampoline activity paused and finishing\" }\n        finish()\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/push/support/HtmlUtils.kt",
    "content": "@file:JvmName(\"HtmlUtils\")\n\npackage com.braze.push.support\n\nimport android.os.Build\nimport android.text.Html\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\n\nprivate val TAG = \"HtmlUtils\".getBrazeLogTag()\n\n/**\n * Returns displayable styled text from the provided HTML\n * string, if [com.braze.configuration.BrazeConfigurationProvider.getIsPushNotificationHtmlRenderingEnabled] is enabled.\n * When disabled, returns the input text.\n */\n@Suppress(\"deprecation\")\nfun String.getHtmlSpannedTextIfEnabled(\n    configurationProvider: BrazeConfigurationProvider\n): CharSequence {\n    if (this.isBlank()) {\n        brazelog(TAG) { \"Cannot create html spanned text on blank text. Returning blank string.\" }\n        return this\n    }\n    return if (configurationProvider.isPushNotificationHtmlRenderingEnabled) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)\n        } else {\n            Html.fromHtml(this)\n        }\n    } else {\n        this\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/BrazeDeeplinkHandler.kt",
    "content": "package com.braze.ui\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport com.braze.enums.Channel\nimport com.braze.IBrazeDeeplinkHandler\nimport com.braze.IBrazeDeeplinkHandler.IntentFlagPurpose\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.actions.NewsfeedAction\nimport com.braze.ui.actions.UriAction\n\nopen class BrazeDeeplinkHandler : IBrazeDeeplinkHandler {\n    override fun gotoNewsFeed(context: Context, newsfeedAction: NewsfeedAction) {\n        newsfeedAction.execute(context)\n    }\n\n    override fun gotoUri(context: Context, uriAction: UriAction) {\n        uriAction.execute(context)\n    }\n\n    override fun getIntentFlags(intentFlagPurpose: IntentFlagPurpose): Int {\n        return when (intentFlagPurpose) {\n            IntentFlagPurpose.NOTIFICATION_ACTION_WITH_DEEPLINK,\n            IntentFlagPurpose.NOTIFICATION_PUSH_STORY_PAGE_CLICK ->\n                Intent.FLAG_ACTIVITY_NO_HISTORY\n            IntentFlagPurpose.URI_ACTION_OPEN_WITH_WEBVIEW_ACTIVITY,\n            IntentFlagPurpose.URI_ACTION_OPEN_WITH_ACTION_VIEW,\n            IntentFlagPurpose.URI_UTILS_GET_MAIN_ACTIVITY_INTENT ->\n                Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP\n            IntentFlagPurpose.URI_ACTION_BACK_STACK_GET_ROOT_INTENT,\n            IntentFlagPurpose.URI_ACTION_BACK_STACK_ONLY_GET_TARGET_INTENT ->\n                Intent.FLAG_ACTIVITY_NEW_TASK\n        }\n    }\n\n    override fun createUriActionFromUrlString(\n        url: String,\n        extras: Bundle?,\n        openInWebView: Boolean,\n        channel: Channel\n    ): UriAction? {\n        return try {\n            if (url.isNotBlank()) {\n                val uri = Uri.parse(url)\n                createUriActionFromUri(uri, extras, openInWebView, channel)\n            } else {\n                brazelog(E) { \"createUriActionFromUrlString url was null. Returning null.\" }\n                null\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"createUriActionFromUrlString failed. Returning null.\" }\n            null\n        }\n    }\n\n    override fun createUriActionFromUri(\n        uri: Uri,\n        extras: Bundle?,\n        openInWebView: Boolean,\n        channel: Channel\n    ) =\n        UriAction(uri, extras, openInWebView, channel)\n\n    companion object {\n        private val defaultHandler: IBrazeDeeplinkHandler = BrazeDeeplinkHandler()\n\n        @Volatile\n        private var customHandler: IBrazeDeeplinkHandler? = null\n\n        /**\n         * Gets the current IBrazeDeeplinkHandler class.\n         *\n         * @return The currently set IBrazeDeeplinkHandler or the default handler if none was set.\n         */\n        @JvmStatic\n        fun getInstance() =\n            customHandler ?: defaultHandler\n\n        /**\n         * Sets a custom BrazeDeeplinkHandler.\n         *\n         * @param brazeDeeplinkHandler The custom IBrazeDeeplinkHandler to be used.\n         */\n        @JvmStatic\n        fun setBrazeDeeplinkHandler(brazeDeeplinkHandler: IBrazeDeeplinkHandler?) {\n            brazelog { \"Custom IBrazeDeeplinkHandler ${if (brazeDeeplinkHandler == null) \"cleared\" else \"set\"}\" }\n            customHandler = brazeDeeplinkHandler\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/BrazeFeedFragment.java",
    "content": "package com.braze.ui;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.GestureDetector;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.AbsListView;\nimport android.widget.LinearLayout;\nimport android.widget.ListView;\nimport android.widget.ProgressBar;\nimport android.widget.RelativeLayout;\n\nimport androidx.annotation.VisibleForTesting;\nimport androidx.core.view.GestureDetectorCompat;\nimport androidx.fragment.app.ListFragment;\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout;\n\nimport com.braze.events.FeedUpdatedEvent;\nimport com.braze.Braze;\nimport com.braze.enums.CardCategory;\nimport com.braze.events.IEventSubscriber;\nimport com.braze.models.cards.Card;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.adapters.BrazeListAdapter;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.List;\n\npublic class BrazeFeedFragment extends ListFragment implements SwipeRefreshLayout.OnRefreshListener {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BrazeFeedFragment.class);\n  private static final int NETWORK_PROBLEM_WARNING_MS = 5000;\n  private static final int MAX_FEED_TTL_SECONDS = 60;\n  private static final long AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS = 2500L;\n  @VisibleForTesting\n  static final String SAVED_INSTANCE_STATE_KEY_PREVIOUS_VISIBLE_HEAD_CARD_INDEX = \"PREVIOUS_VISIBLE_HEAD_CARD_INDEX\";\n  @VisibleForTesting\n  static final String SAVED_INSTANCE_STATE_KEY_CURRENT_CARD_INDEX_AT_BOTTOM_OF_SCREEN = \"CURRENT_CARD_INDEX_AT_BOTTOM_OF_SCREEN\";\n  @VisibleForTesting\n  static final String SAVED_INSTANCE_STATE_KEY_SKIP_CARD_IMPRESSIONS_RESET = \"SKIP_CARD_IMPRESSIONS_RESET\";\n  @VisibleForTesting\n  static final String SAVED_INSTANCE_STATE_KEY_CARD_CATEGORY = \"CARD_CATEGORY\";\n\n  private final Handler mMainThreadLooper = new Handler(Looper.getMainLooper());\n  // Shows the network error message. This should only be executed on the Main/UI thread.\n  private final Runnable mShowNetworkError = new Runnable() {\n    @Override\n    public void run() {\n      // null checks make sure that this only executes when the constituent views are valid references.\n      if (mLoadingSpinner != null) {\n        mLoadingSpinner.setVisibility(View.GONE);\n      }\n      if (mNetworkErrorLayout != null) {\n        mNetworkErrorLayout.setVisibility(View.VISIBLE);\n      }\n    }\n  };\n\n  private IEventSubscriber<FeedUpdatedEvent> mFeedUpdatedSubscriber;\n  private BrazeListAdapter mAdapter;\n  private LinearLayout mNetworkErrorLayout;\n  private LinearLayout mEmptyFeedLayout;\n  private ProgressBar mLoadingSpinner;\n  private RelativeLayout mFeedRootLayout;\n  private EnumSet<CardCategory> mCategories;\n  private SwipeRefreshLayout mFeedSwipeLayout;\n  private GestureDetectorCompat mGestureDetector;\n  private boolean mSortEnabled = false;\n\n  @VisibleForTesting\n  boolean mSkipCardImpressionsReset = false;\n  @VisibleForTesting\n  int mPreviousVisibleHeadCardIndex = 0;\n  @VisibleForTesting\n  int mCurrentCardIndexAtBottomOfScreen = 0;\n\n  // This view should only be in the View.VISIBLE state when the listview is not visible. This view's\n  // purpose is to let the \"network error\" and \"no card\" states to have the swipe-to-refresh functionality\n  // when their respective views are visible.\n  private View mTransparentFullBoundsContainerView;\n\n  @Override\n  public void onAttach(final Context context) {\n    super.onAttach(context);\n    if (mAdapter == null) {\n      mAdapter = new BrazeListAdapter(context, R.id.tag, new ArrayList<>());\n      mCategories = CardCategory.getAllCategories();\n    }\n    mGestureDetector = new GestureDetectorCompat(context, new FeedGestureListener());\n  }\n\n  @Override\n  public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) {\n    View view = layoutInflater.inflate(R.layout.com_braze_feed, container, false);\n    mNetworkErrorLayout = view.findViewById(R.id.com_braze_feed_network_error);\n    mLoadingSpinner = view.findViewById(R.id.com_braze_feed_loading_spinner);\n    mEmptyFeedLayout = view.findViewById(R.id.com_braze_feed_empty_feed);\n    mFeedRootLayout = view.findViewById(R.id.com_braze_feed_root);\n    mFeedSwipeLayout = view.findViewById(R.id.braze_feed_swipe_container);\n    mFeedSwipeLayout.setOnRefreshListener(this);\n    mFeedSwipeLayout.setEnabled(false);\n    mFeedSwipeLayout.setColorSchemeResources(R.color.com_braze_newsfeed_swipe_refresh_color_1,\n        R.color.com_braze_newsfeed_swipe_refresh_color_2,\n        R.color.com_braze_newsfeed_swipe_refresh_color_3,\n        R.color.com_braze_newsfeed_swipe_refresh_color_4);\n    mTransparentFullBoundsContainerView = view.findViewById(R.id.com_braze_feed_transparent_full_bounds_container_view);\n    return view;\n  }\n\n  @SuppressLint(\"InflateParams\")\n  @Override\n  public void onViewCreated(View view, Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n    loadFragmentStateFromSavedInstanceState(savedInstanceState);\n    if (mSkipCardImpressionsReset) {\n      mSkipCardImpressionsReset = false;\n    } else {\n      mAdapter.resetCardImpressionTracker();\n      BrazeLogger.d(TAG, \"Resetting card impressions.\");\n    }\n\n    // Applying top and bottom padding as header and footer views allows for the top and bottom padding to be scrolled\n    // away, as opposed to being a permanent frame around the feed.\n    LayoutInflater inflater = LayoutInflater.from(getActivity());\n    final ListView listView = getListView();\n    // Inflating these views without a view root is valid since they have no parent layout information\n    // that needs to be respected.\n    listView.addHeaderView(inflater.inflate(R.layout.com_braze_feed_header, null));\n    listView.addFooterView(inflater.inflate(R.layout.com_braze_feed_footer, null));\n\n    mFeedRootLayout.setOnTouchListener((currentView, motionEvent) -> {\n      // Send touch events from the background view to the gesture detector to enable margin listview scrolling\n      return mGestureDetector.onTouchEvent(motionEvent);\n    });\n\n    // Enable the swipe-to-refresh view only when the user is at the head of the listview.\n    listView.setOnScrollListener(new AbsListView.OnScrollListener() {\n      @Override\n      public void onScrollStateChanged(AbsListView absListView, int scrollState) {\n      }\n\n      @Override\n      public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n        mFeedSwipeLayout.setEnabled(firstVisibleItem == 0);\n\n        // Handle read/unread cards functionality below\n        if (visibleItemCount == 0) {\n          // No cards/views have been loaded, do nothing\n          return;\n        }\n\n        int currentVisibleHeadCardIndex = firstVisibleItem - 1;\n\n        // Head index increased (scroll down)\n        if (currentVisibleHeadCardIndex > mPreviousVisibleHeadCardIndex) {\n          // Mark all cards in the gap as read\n          mAdapter.batchSetCardsToRead(mPreviousVisibleHeadCardIndex, currentVisibleHeadCardIndex);\n        }\n        mPreviousVisibleHeadCardIndex = currentVisibleHeadCardIndex;\n\n        // We take note of what card is at the bottom of the feed so that when this fragment is destroyed,\n        // all on-screen cards have updated read indicators.\n        mCurrentCardIndexAtBottomOfScreen = firstVisibleItem + visibleItemCount;\n      }\n    });\n\n    // We need the transparent view to pass it's touch events to the swipe-to-refresh view. We\n    // do this by consuming touch events in the transparent view.\n    mTransparentFullBoundsContainerView.setOnTouchListener((currentView, motionEvent) -> {\n      // Only consume events if the view is visible\n      return currentView.getVisibility() == View.VISIBLE;\n    });\n\n    // Remove the previous subscriber before rebuilding a new one with our new activity.\n    Braze.getInstance(getContext()).removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);\n    mFeedUpdatedSubscriber = event -> {\n      Activity activity = getActivity();\n      // Not strictly necessary, but being defensive in the face of a lot of inconsistent behavior with\n      // fragment/activity lifecycles.\n      if (activity == null) {\n        return;\n      }\n\n      activity.runOnUiThread(() -> {\n        BrazeLogger.v(TAG, \"Updating feed views in response to FeedUpdatedEvent: \" + event);\n        // If a FeedUpdatedEvent comes in, we make sure that the network error isn't visible. It could become\n        // visible again later if we need to request a new feed and it doesn't return in time, but we display a\n        // network spinner while we wait, instead of keeping the network error up.\n        mMainThreadLooper.removeCallbacks(mShowNetworkError);\n        mNetworkErrorLayout.setVisibility(View.GONE);\n\n        // If there are no cards, regardless of what happens further down, we're not going to show the list view, so\n        // clear the list view and change relevant visibility now.\n        if (event.getCardCount(mCategories) == 0) {\n          listView.setVisibility(View.GONE);\n          mAdapter.clear();\n        } else {\n          mEmptyFeedLayout.setVisibility(View.GONE);\n          mLoadingSpinner.setVisibility(View.GONE);\n          mTransparentFullBoundsContainerView.setVisibility(View.GONE);\n        }\n\n        // If we got our feed from offline storage, and it was old, we asynchronously request a new one from the server,\n        // putting up a spinner if the old feed was empty.\n        if (event.isFromOfflineStorage() && (event.lastUpdatedInSecondsFromEpoch() + MAX_FEED_TTL_SECONDS) * 1000 < System.currentTimeMillis()) {\n          BrazeLogger.i(TAG, \"Feed received was older than the max time to live of \" + MAX_FEED_TTL_SECONDS + \" seconds, displaying it \"\n              + \"for now, but requesting an updated view from the server.\");\n          Braze.getInstance(getContext()).requestFeedRefresh();\n          // If we don't have any cards to display, we put up the spinner while we wait for the network to return.\n          // Eventually displaying an error message if it doesn't.\n          if (event.getCardCount(mCategories) == 0) {\n            BrazeLogger.d(TAG, \"Old feed was empty, putting up a network spinner and registering the network \"\n                + \"error message with a delay of \" + NETWORK_PROBLEM_WARNING_MS + \"ms.\");\n            mEmptyFeedLayout.setVisibility(View.GONE);\n            mLoadingSpinner.setVisibility(View.VISIBLE);\n            mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);\n            mMainThreadLooper.postDelayed(mShowNetworkError, NETWORK_PROBLEM_WARNING_MS);\n            return;\n          }\n        }\n\n        // If we get here, we know that our feed is either fresh from the cache, or came down directly from a\n        // network request. Thus, an empty feed shouldn't have a network error, or a spinner, we should just\n        // tell the user that the feed is empty.\n        if (event.getCardCount(mCategories) == 0) {\n          mLoadingSpinner.setVisibility(View.GONE);\n          mEmptyFeedLayout.setVisibility(View.VISIBLE);\n          mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);\n        } else {\n          if (mSortEnabled && event.getCardCount(mCategories) != event.getUnreadCardCount(mCategories)) {\n            mAdapter.replaceFeed(sortFeedCards(event.getFeedCards(mCategories)));\n          } else {\n            mAdapter.replaceFeed(event.getFeedCards(mCategories));\n          }\n          listView.setVisibility(View.VISIBLE);\n        }\n\n        mFeedSwipeLayout.setRefreshing(false);\n      });\n    };\n    Braze.getInstance(getContext()).subscribeToFeedUpdates(mFeedUpdatedSubscriber);\n\n    // Once the header and footer views are set and our event handlers are ready to go, we set the adapter and hit the\n    // cache for an initial feed load.\n    listView.setAdapter(mAdapter);\n    Braze.getInstance(getContext()).requestFeedRefreshFromCache();\n  }\n\n  /**\n   * The sortFeedCards is responsible for sorting newsfeed cards depending on whether or not they have already been viewed.\n   * It is only run when the the mSortEnabled is set to true and its expected behavior is to maintain the respective order of cards\n   * which have the same view status.\n   */\n  public List<Card> sortFeedCards(List<Card> cards) {\n    Collections.sort(cards, (cardOne, cardTwo) -> (cardOne.isIndicatorHighlighted() == cardTwo.isIndicatorHighlighted() ? 0 : (cardOne.isIndicatorHighlighted() ? 1 : -1)));\n    return cards;\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    Braze.getInstance(getContext()).logFeedDisplayed();\n  }\n\n  @Override\n  public void onDestroyView() {\n    super.onDestroyView();\n    // If the view is destroyed, we don't care about updating it anymore. Remove the subscription immediately.\n    Braze.getInstance(getContext()).removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);\n    setOnScreenCardsToRead();\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    setOnScreenCardsToRead();\n  }\n\n  /**\n   * This should be called whenever the feed goes off the user's screen.\n   */\n  private void setOnScreenCardsToRead() {\n    // Set whatever cards are on screen to read since the view is being destroyed.\n    mAdapter.batchSetCardsToRead(mPreviousVisibleHeadCardIndex, mCurrentCardIndexAtBottomOfScreen);\n  }\n\n  @Override\n  public void onDetach() {\n    super.onDetach();\n    setListAdapter(null);\n  }\n\n  /**\n   * The onSaveInstanceState method gets called before an orientation change when either the fragment is\n   * the current fragment or exists in the fragment manager backstack.\n   */\n  @Override\n  public void onSaveInstanceState(Bundle outState) {\n    // Save the state of this instance into the outState bundle\n    outState.putInt(SAVED_INSTANCE_STATE_KEY_PREVIOUS_VISIBLE_HEAD_CARD_INDEX, mPreviousVisibleHeadCardIndex);\n    outState.putInt(SAVED_INSTANCE_STATE_KEY_CURRENT_CARD_INDEX_AT_BOTTOM_OF_SCREEN, mCurrentCardIndexAtBottomOfScreen);\n    outState.putBoolean(SAVED_INSTANCE_STATE_KEY_SKIP_CARD_IMPRESSIONS_RESET, mSkipCardImpressionsReset);\n\n    if (mCategories == null) {\n      mCategories = CardCategory.getAllCategories();\n    }\n    // An arraylist containing the ordinals of each CardCategory enum value\n    ArrayList<String> cardCategoryArrayList = new ArrayList<>(mCategories.size());\n\n    for (CardCategory cardCategory : mCategories) {\n      cardCategoryArrayList.add(cardCategory.name());\n    }\n    outState.putStringArrayList(SAVED_INSTANCE_STATE_KEY_CARD_CATEGORY, cardCategoryArrayList);\n    super.onSaveInstanceState(outState);\n    // We set mSkipCardImpressionsReset to true only when onSaveInstanceState is called while the fragment\n    // is visible on the screen. That happens when the fragment is being managed by the fragment manager and\n    // it is not in the backstack. We do this to avoid setting the mSkipCardImpressionsReset flag when the\n    // device undergoes an orientation change while the fragment is in the backstack.\n    if (isVisible()) {\n      mSkipCardImpressionsReset = true;\n    }\n  }\n\n  /**\n   * Unpacks the data from a bundle marshalled in onSaveInstanceState due to a configuration change.\n   */\n  @VisibleForTesting\n  void loadFragmentStateFromSavedInstanceState(Bundle savedInstanceState) {\n    if (savedInstanceState == null) {\n      // There's no previous state to load from, so just return.\n      return;\n    }\n    if (mCategories == null) {\n      mCategories = CardCategory.getAllCategories();\n    }\n    mPreviousVisibleHeadCardIndex = savedInstanceState.getInt(SAVED_INSTANCE_STATE_KEY_PREVIOUS_VISIBLE_HEAD_CARD_INDEX, 0);\n    mCurrentCardIndexAtBottomOfScreen = savedInstanceState.getInt(SAVED_INSTANCE_STATE_KEY_CURRENT_CARD_INDEX_AT_BOTTOM_OF_SCREEN, 0);\n    mSkipCardImpressionsReset = savedInstanceState.getBoolean(SAVED_INSTANCE_STATE_KEY_SKIP_CARD_IMPRESSIONS_RESET, false);\n\n    ArrayList<String> cardCategoryArrayList = savedInstanceState.getStringArrayList(SAVED_INSTANCE_STATE_KEY_CARD_CATEGORY);\n    if (cardCategoryArrayList != null) {\n      mCategories.clear();\n      for (String cardCategoryString: cardCategoryArrayList) {\n        mCategories.add(CardCategory.valueOf(cardCategoryString));\n      }\n    }\n  }\n\n  public EnumSet<CardCategory> getCategories() {\n    return mCategories;\n  }\n\n  public boolean getSortEnabled() {\n    return mSortEnabled;\n  }\n\n  /**\n   * The setSortEnabled methods sets the mSortEnabled bool which determines whether or not on update we sort\n   * newsfeed cards by their read status. Sorting is currently not done by default on requestFeedRefreshFromCache.\n   */\n  public void setSortEnabled(boolean sortEnabled) {\n    mSortEnabled = sortEnabled;\n  }\n\n  public void setCategory(CardCategory category) {\n    setCategories(EnumSet.of(category));\n  }\n\n  /**\n   * Calling this method will make BrazeFeedFragment display a list of cards where each card belongs\n   * to at least one of the given categories.\n   * When there are no cards in those categories, this method returns an empty list.\n   * When the passed in categories are null, all cards will be returned.\n   * When the passed in categories are empty EnumSet, an empty list will be returned.\n   *\n   * @param categories an EnumSet of CardCategory. Please pass in a non-empty EnumSet of CardCategory,\n   *                   or a null. An empty EnumSet is considered invalid.\n   */\n  public void setCategories(EnumSet<CardCategory> categories) {\n    if (categories == null) {\n      BrazeLogger.i(TAG, \"The categories passed into setCategories are null, BrazeFeedFragment is going to display all the cards in cache.\");\n      mCategories = CardCategory.getAllCategories();\n    } else if (categories.isEmpty()) {\n      BrazeLogger.w(TAG, \"The categories set had no elements and have been ignored. Please pass a valid EnumSet of CardCategory.\");\n      return;\n    } else if (categories.equals(mCategories)) {\n      return;\n    } else {\n      mCategories = categories;\n    }\n    if (Braze.getInstance(getContext()) != null) {\n      Braze.getInstance(getContext()).requestFeedRefreshFromCache();\n    }\n  }\n\n  /**\n   * Called when the user swipes down and requests a feed refresh.\n   */\n  @Override\n  public void onRefresh() {\n    Braze.getInstance(getContext()).requestFeedRefresh();\n    mMainThreadLooper.postDelayed(() -> mFeedSwipeLayout.setRefreshing(false), AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS);\n  }\n\n  /**\n   * This class is a custom listener to catch gestures happening outside the bounds of the listview that\n   * should be fed into it.\n   */\n  public class FeedGestureListener extends GestureDetector.SimpleOnGestureListener {\n    @Override\n    public boolean onDown(MotionEvent motionEvent) {\n      return true;\n    }\n\n    @Override\n    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent2, float dx, float dy) {\n      getListView().smoothScrollBy((int) dy, 0);\n      return true;\n    }\n\n    @Override\n    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float velocityX, float velocityY) {\n      // We need to find the pixel distance of the scroll from the velocity with units (px / sec)\n      // So d (px) = v (px / sec) * 1 (sec) / 1000 (ms) * deltaTimeMillis (ms)\n      long deltaTimeMillis = (motionEvent2.getEventTime() - motionEvent.getEventTime()) * 2;\n      int scrollDistance = (int) (velocityY * deltaTimeMillis / 1000);\n      // Multiplied by 2 to get a smoother scroll effect during a fling\n      getListView().smoothScrollBy(-scrollDistance, (int) (deltaTimeMillis * 2));\n      return true;\n    }\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/BrazeWebViewActivity.kt",
    "content": "package com.braze.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.View\nimport android.view.WindowManager\nimport android.webkit.ConsoleMessage\nimport android.webkit.RenderProcessGoneDetail\nimport android.webkit.WebChromeClient\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebSettings\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.annotation.RequiresApi\nimport androidx.fragment.app.FragmentActivity\nimport com.braze.Constants\nimport com.braze.enums.Channel\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.REMOTE_SCHEMES\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance\nimport com.braze.ui.actions.IAction\nimport com.braze.ui.support.isDeviceInNightMode\n\n/**\n * Note that this Activity is not and should not be exported by default in\n * the AndroidManifest so external applications are not able to pass\n * arbitrary URLs via this intent.\n *\n * From https://developer.android.com/guide/topics/manifest/activity-element#exported ->\n * \"If \"false\", the activity can be launched only by components of the same\n * application or applications with the same user ID.\"\n */\n@SuppressLint(\"SetJavaScriptEnabled\")\nopen class BrazeWebViewActivity : FragmentActivity() {\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // Enables hardware acceleration for the window.\n        // See https://developer.android.com/guide/topics/graphics/hardware-accel.html#controlling.\n        // With this flag, we can view Youtube videos since HTML5 requires hardware acceleration.\n        window.setFlags(\n            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,\n            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED\n        )\n        setContentView(R.layout.com_braze_webview_activity)\n        val webView = findViewById<WebView>(R.id.com_braze_webview_activity_webview)\n        webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)\n        val webSettings = webView.settings\n        webSettings.allowFileAccess = false\n        webSettings.builtInZoomControls = true\n        webSettings.javaScriptEnabled = true\n        webSettings.useWideViewPort = true\n        webSettings.loadWithOverviewMode = true\n        webSettings.displayZoomControls = false\n        webSettings.domStorageEnabled = true\n        if (isDeviceInNightMode(this.applicationContext)) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                webSettings.isAlgorithmicDarkeningAllowed = true\n            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                @Suppress(\"DEPRECATION\")\n                webSettings.forceDark = WebSettings.FORCE_DARK_ON\n            }\n        }\n        webView.webChromeClient = createWebChromeClient()\n        webView.webViewClient = createWebViewClient()\n\n        // Opens the URL passed as an intent extra (if one exists).\n        intent.extras?.getString(Constants.BRAZE_WEBVIEW_URL_EXTRA)?.let { url ->\n            webView.loadUrl(url)\n        }\n    }\n\n    open fun createWebChromeClient(): WebChromeClient {\n        return object : WebChromeClient() {\n            override fun onConsoleMessage(cm: ConsoleMessage): Boolean {\n                brazelog {\n                    \"Braze WebView Activity log. Line: ${cm.lineNumber()}. SourceId: ${cm.sourceId()}. \" +\n                        \"Log Level: ${cm.messageLevel()}. Message: ${cm.message()}\"\n                }\n                return true\n            }\n\n            override fun getDefaultVideoPoster(): Bitmap? {\n                // This bitmap is used to eliminate the default black & white\n                // play icon used as the default poster.\n                return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)\n            }\n        }\n    }\n\n    open fun createWebViewClient(): WebViewClient {\n        return object : WebViewClient() {\n            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n            override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {\n                val didHandleUrl = handleUrlOverride(view.context, request.url.toString())\n                return didHandleUrl ?: super.shouldOverrideUrlLoading(view, request)\n            }\n\n            @Deprecated(\"Deprecated in Java\")\n            @Suppress(\"deprecation\")\n            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {\n                val didHandleUrl = handleUrlOverride(view.context, url)\n                return didHandleUrl ?: super.shouldOverrideUrlLoading(view, url)\n            }\n\n            /**\n             * Handles the URL override when the link is not better handled by this WebView instead.\n             *\n             * E.g. in the presence of a https link, this WebView should handle it and not\n             * forward to Chrome. However in the presence of a sms link, the system itself\n             * should handle it since a WebView lacks that ability.\n             *\n             * @return True/False when this URL override was handled. Returns null when the super implementation of\n             * [WebViewClient.shouldOverrideUrlLoading] should be called instead.\n             */\n            private fun handleUrlOverride(context: Context, url: String): Boolean? {\n                try {\n                    if (REMOTE_SCHEMES.contains(Uri.parse(url).scheme)) {\n                        return null\n                    }\n                    val action: IAction? = getInstance().createUriActionFromUrlString(url, intent.extras, false, Channel.UNKNOWN)\n                    return if (action != null) {\n                        // Instead of using BrazeDeeplinkHandler, just open directly.\n                        action.execute(context)\n\n                        // Close the WebView if the action was executed successfully\n                        finish()\n                        true\n                    } else {\n                        false\n                    }\n                } catch (e: Exception) {\n                    brazelog(E, e) { \"Unexpected exception while processing url $url. Passing url back to WebView.\" }\n                }\n                return null\n            }\n\n            override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {\n                brazelog(I) { \"The webview rendering process crashed, returning true\" }\n\n                // The app crashes after detecting the renderer crashed. Returning true to avoid app crash.\n                return true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/BrazeXamarinFormsFeedFragment.java",
    "content": "package com.braze.ui;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.GestureDetector;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.AbsListView;\nimport android.widget.LinearLayout;\nimport android.widget.ListView;\nimport android.widget.ProgressBar;\nimport android.widget.RelativeLayout;\n\nimport androidx.core.view.GestureDetectorCompat;\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout;\n\nimport com.braze.events.FeedUpdatedEvent;\nimport com.braze.Braze;\nimport com.braze.IBraze;\nimport com.braze.enums.CardCategory;\nimport com.braze.events.IEventSubscriber;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.adapters.BrazeListAdapter;\n\nimport java.util.ArrayList;\nimport java.util.EnumSet;\n\n@SuppressWarnings({\"PMD.UnnecessaryConstructor\", \"deprecation\"})\npublic class BrazeXamarinFormsFeedFragment extends android.app.ListFragment implements SwipeRefreshLayout.OnRefreshListener {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BrazeXamarinFormsFeedFragment.class);\n  private static final int NETWORK_PROBLEM_WARNING_MS = 5000;\n  private static final int MAX_FEED_TTL_SECONDS = 60;\n  private static final long AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS = 2500L;\n\n  private final Handler mMainThreadLooper = new Handler(Looper.getMainLooper());\n  // Shows the network error message. This should only be executed on the Main/UI thread.\n  private final Runnable mShowNetworkError = new Runnable() {\n    @Override\n    public void run() {\n      // null checks make sure that this only executes when the constituent views are valid references.\n      if (mLoadingSpinner != null) {\n        mLoadingSpinner.setVisibility(View.GONE);\n      }\n      if (mNetworkErrorLayout != null) {\n        mNetworkErrorLayout.setVisibility(View.VISIBLE);\n      }\n    }\n  };\n\n  private IBraze mBraze;\n  private IEventSubscriber<FeedUpdatedEvent> mFeedUpdatedSubscriber;\n  private BrazeListAdapter mAdapter;\n  private LinearLayout mNetworkErrorLayout;\n  private LinearLayout mEmptyFeedLayout;\n  private ProgressBar mLoadingSpinner;\n  private RelativeLayout mFeedRootLayout;\n  private boolean mSkipCardImpressionsReset;\n  private EnumSet<CardCategory> mCategories;\n  private SwipeRefreshLayout mFeedSwipeLayout;\n  private int previousVisibleHeadCardIndex;\n  private int currentCardIndexAtBottomOfScreen;\n  private GestureDetectorCompat mGestureDetector;\n\n  // This view should only be in the View.VISIBLE state when the listview is not visible. This view's\n  // purpose is to let the \"network error\" and \"no card\" states to have the swipe-to-refresh functionality\n  // when their respective views are visible.\n  private View mTransparentFullBoundsContainerView;\n\n  public BrazeXamarinFormsFeedFragment() {\n  }\n\n  @Override\n  public void onAttach(final Context context) {\n    super.onAttach(context);\n    mBraze = Braze.getInstance(context);\n    if (mAdapter == null) {\n      mAdapter = new BrazeListAdapter(context, R.id.tag, new ArrayList<>());\n      mCategories = CardCategory.getAllCategories();\n    }\n    setRetainInstance(true);\n    mGestureDetector = new GestureDetectorCompat(context, new FeedGestureListener());\n  }\n\n  @Override\n  public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) {\n    View view = layoutInflater.inflate(R.layout.com_braze_feed, container, false);\n    mNetworkErrorLayout = view.findViewById(R.id.com_braze_feed_network_error);\n    mLoadingSpinner = view.findViewById(R.id.com_braze_feed_loading_spinner);\n    mEmptyFeedLayout = view.findViewById(R.id.com_braze_feed_empty_feed);\n    mFeedRootLayout = view.findViewById(R.id.com_braze_feed_root);\n    mFeedSwipeLayout = view.findViewById(R.id.braze_feed_swipe_container);\n    mFeedSwipeLayout.setOnRefreshListener(this);\n    mFeedSwipeLayout.setEnabled(false);\n    mFeedSwipeLayout.setColorSchemeResources(R.color.com_braze_newsfeed_swipe_refresh_color_1,\n        R.color.com_braze_newsfeed_swipe_refresh_color_2,\n        R.color.com_braze_newsfeed_swipe_refresh_color_3,\n        R.color.com_braze_newsfeed_swipe_refresh_color_4);\n    mTransparentFullBoundsContainerView = view.findViewById(R.id.com_braze_feed_transparent_full_bounds_container_view);\n    return view;\n  }\n\n  @SuppressLint(\"InflateParams\")\n  @Override\n  public void onActivityCreated(Bundle savedInstanceState) {\n    super.onActivityCreated(savedInstanceState);\n    if (mSkipCardImpressionsReset) {\n      mSkipCardImpressionsReset = false;\n    } else {\n      mAdapter.resetCardImpressionTracker();\n      BrazeLogger.d(TAG, \"Resetting card impressions.\");\n    }\n\n    // Applying top and bottom padding as header and footer views allows for the top and bottom padding to be scrolled\n    // away, as opposed to being a permanent frame around the feed.\n    LayoutInflater inflater = LayoutInflater.from(getActivity());\n    final ListView listView = getListView();\n    listView.addHeaderView(inflater.inflate(R.layout.com_braze_feed_header, null));\n    listView.addFooterView(inflater.inflate(R.layout.com_braze_feed_footer, null));\n\n    mFeedRootLayout.setOnTouchListener((view, motionEvent) -> {\n      // Send touch events from the background view to the gesture detector to enable margin listview scrolling\n      return mGestureDetector.onTouchEvent(motionEvent);\n    });\n\n    // Enable the swipe-to-refresh view only when the user is at the head of the listview.\n    listView.setOnScrollListener(new AbsListView.OnScrollListener() {\n      @Override\n      public void onScrollStateChanged(AbsListView absListView, int scrollState) {\n      }\n\n      @Override\n      public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n        mFeedSwipeLayout.setEnabled(firstVisibleItem == 0);\n\n        // Handle read/unread cards functionality below\n        if (visibleItemCount == 0) {\n          // No cards/views have been loaded, do nothing\n          return;\n        }\n\n        int currentVisibleHeadCardIndex = firstVisibleItem - 1;\n\n        // Head index increased (scroll down)\n        if (currentVisibleHeadCardIndex > previousVisibleHeadCardIndex) {\n          // Mark all cards in the gap as read\n          mAdapter.batchSetCardsToRead(previousVisibleHeadCardIndex, currentVisibleHeadCardIndex);\n        }\n        previousVisibleHeadCardIndex = currentVisibleHeadCardIndex;\n\n        // We take note of what card is at the bottom of the feed so that when this fragment is destroyed,\n        // all on-screen cards have updated read indicators.\n        currentCardIndexAtBottomOfScreen = firstVisibleItem + visibleItemCount;\n      }\n    });\n\n    // We need the transparent view to pass it's touch events to the swipe-to-refresh view. We\n    // do this by consuming touch events in the transparent view.\n    mTransparentFullBoundsContainerView.setOnTouchListener((view, motionEvent) -> {\n      // Only consume events if the view is visible\n      return view.getVisibility() == View.VISIBLE;\n    });\n\n    // Remove the previous subscriber before rebuilding a new one with our new activity.\n    mBraze.removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);\n    mFeedUpdatedSubscriber = event -> {\n      Activity activity = getActivity();\n      // Not strictly necessary, but being defensive in the face of a lot of inconsistent behavior with\n      // fragment/activity lifecycles.\n      if (activity == null) {\n        return;\n      }\n\n      activity.runOnUiThread(() -> {\n        BrazeLogger.d(TAG, \"Updating feed views in response to FeedUpdatedEvent: \" + event);\n        // If a FeedUpdatedEvent comes in, we make sure that the network error isn't visible. It could become\n        // visible again later if we need to request a new feed and it doesn't return in time, but we display a\n        // network spinner while we wait, instead of keeping the network error up.\n        mMainThreadLooper.removeCallbacks(mShowNetworkError);\n        mNetworkErrorLayout.setVisibility(View.GONE);\n\n        // If there are no cards, regardless of what happens further down, we're not going to show the list view, so\n        // clear the list view and change relevant visibility now.\n        if (event.getCardCount(mCategories) == 0) {\n          listView.setVisibility(View.GONE);\n          mAdapter.clear();\n        } else {\n          mEmptyFeedLayout.setVisibility(View.GONE);\n          mLoadingSpinner.setVisibility(View.GONE);\n          mTransparentFullBoundsContainerView.setVisibility(View.GONE);\n        }\n\n        // If we got our feed from offline storage, and it was old, we asynchronously request a new one from the server,\n        // putting up a spinner if the old feed was empty.\n        if (event.isFromOfflineStorage() && (event.lastUpdatedInSecondsFromEpoch() + MAX_FEED_TTL_SECONDS) * 1000 < System.currentTimeMillis()) {\n          BrazeLogger.i(TAG, \"Feed received was older than the max time to live of \" + MAX_FEED_TTL_SECONDS + \" seconds, displaying it \"\n              + \"for now, but requesting an updated view from the server.\");\n          mBraze.requestFeedRefresh();\n          // If we don't have any cards to display, we put up the spinner while we wait for the network to return.\n          // Eventually displaying an error message if it doesn't.\n          if (event.getCardCount(mCategories) == 0) {\n            BrazeLogger.d(TAG, \"Old feed was empty, putting up a network spinner and registering the network error message on a delay of \" + NETWORK_PROBLEM_WARNING_MS + \"ms.\");\n            mEmptyFeedLayout.setVisibility(View.GONE);\n            mLoadingSpinner.setVisibility(View.VISIBLE);\n            mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);\n            mMainThreadLooper.postDelayed(mShowNetworkError, NETWORK_PROBLEM_WARNING_MS);\n            return;\n          }\n        }\n\n        // If we get here, we know that our feed is either fresh from the cache, or came down directly from a\n        // network request. Thus, an empty feed shouldn't have a network error, or a spinner, we should just\n        // tell the user that the feed is empty.\n        if (event.getCardCount(mCategories) == 0) {\n          mLoadingSpinner.setVisibility(View.GONE);\n          mEmptyFeedLayout.setVisibility(View.VISIBLE);\n          mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);\n        } else {\n          mAdapter.replaceFeed(event.getFeedCards(mCategories));\n          listView.setVisibility(View.VISIBLE);\n        }\n        mFeedSwipeLayout.setRefreshing(false);\n      });\n    };\n    mBraze.subscribeToFeedUpdates(mFeedUpdatedSubscriber);\n\n    // Once the header and footer views are set and our event handlers are ready to go, we set the adapter and hit the\n    // cache for an initial feed load.\n    listView.setAdapter(mAdapter);\n    mBraze.requestFeedRefreshFromCache();\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    Braze.getInstance(getActivity()).logFeedDisplayed();\n  }\n\n  @Override\n  public void onDestroyView() {\n    super.onDestroyView();\n    // If the view is destroyed, we don't care about updating it anymore. Remove the subscription immediately.\n    mBraze.removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);\n\n    setOnScreenCardsToRead();\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    setOnScreenCardsToRead();\n  }\n\n  /**\n   * This should be called whenever the feed goes off the user's screen.\n   */\n  private void setOnScreenCardsToRead() {\n    // Set whatever cards are on screen to read since the view is being destroyed.\n    mAdapter.batchSetCardsToRead(previousVisibleHeadCardIndex, currentCardIndexAtBottomOfScreen);\n  }\n\n  @Override\n  public void onDetach() {\n    super.onDetach();\n    setListAdapter(null);\n  }\n\n  // The onSaveInstanceState method gets called before an orientation change when either the fragment is\n  // the current fragment or exists in the fragment manager backstack.\n  @Override\n  public void onSaveInstanceState(Bundle outState) {\n    super.onSaveInstanceState(outState);\n    // We set mSkipCardImpressionsReset to true only when onSaveInstanceState is called while the fragment\n    // is visible on the screen. That happens when the fragment is being managed by the fragment manager and\n    // it is not in the backstack. We do this to avoid setting the mSkipCardImpressionsReset flag when the\n    // device undergoes an orientation change while the fragment is in the backstack.\n    if (isVisible()) {\n      mSkipCardImpressionsReset = true;\n    }\n  }\n\n  public EnumSet<CardCategory> getCategories() {\n    return mCategories;\n  }\n\n  public void setCategory(CardCategory category) {\n    setCategories(EnumSet.of(category));\n  }\n\n  /**\n   * Calling this method will make BrazeFeedFragment display a list of cards where each card belongs\n   * to at least one of the given categories.\n   * When there are no cards in those categories, this method returns an empty list.\n   * When the passed in categories are null, all cards will be returned.\n   * When the passed in categories are empty EnumSet, an empty list will be returned.\n   *\n   * @param categories an EnumSet of CardCategory. Please pass in a non-empty EnumSet of CardCategory,\n   *                   or a null. An empty EnumSet is considered invalid.\n   */\n  public void setCategories(EnumSet<CardCategory> categories) {\n    if (categories == null) {\n      BrazeLogger.i(TAG, \"The categories passed into setCategories are null, BrazeFeedFragment is going to display all the cards in cache.\");\n      mCategories = CardCategory.getAllCategories();\n    } else if (categories.isEmpty()) {\n      BrazeLogger.w(TAG, \"The categories set had no elements and have been ignored. Please pass a valid EnumSet of CardCategory.\");\n      return;\n    } else if (categories.equals(mCategories)) {\n      return;\n    } else {\n      mCategories = categories;\n    }\n    if (mBraze != null) {\n      mBraze.requestFeedRefreshFromCache();\n    }\n  }\n\n  // Called when the user swipes down and requests a feed refresh.\n  @Override\n  public void onRefresh() {\n    mBraze.requestFeedRefresh();\n    mMainThreadLooper.postDelayed(() -> mFeedSwipeLayout.setRefreshing(false), AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS);\n  }\n\n  // This class is a custom listener to catch gestures happening outside the bounds of the listview that\n  // should be fed into it.\n  public class FeedGestureListener extends GestureDetector.SimpleOnGestureListener {\n    @Override\n    public boolean onDown(MotionEvent motionEvent) {\n      return true;\n    }\n\n    @Override\n    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent2, float dx, float dy) {\n      getListView().smoothScrollBy((int) dy, 0);\n      return true;\n    }\n\n    @Override\n    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float velocityX, float velocityY) {\n      // We need to find the pixel distance of the scroll from the velocity with units (px / sec)\n      // So d (px) = v (px / sec) * 1 (sec) / 1000 (ms) * deltaTimeMillis (ms)\n      long deltaTimeMillis = (motionEvent2.getEventTime() - motionEvent.getEventTime()) * 2;\n      int scrollDistance = (int) (velocityY * deltaTimeMillis / 1000);\n      // Multiplied by 2 to get a smoother scroll effect during a fling\n      getListView().smoothScrollBy(-scrollDistance, (int) (deltaTimeMillis * 2));\n      return true;\n    }\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/IAction.kt",
    "content": "package com.braze.ui.actions\n\nimport android.content.Context\nimport com.braze.enums.Channel\n\ninterface IAction {\n    val channel: Channel\n\n    fun execute(context: Context)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/NewsfeedAction.kt",
    "content": "package com.braze.ui.actions\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.enums.Channel\nimport com.braze.ui.activities.BrazeFeedActivity\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\n\nopen class NewsfeedAction(val extras: Bundle?, override val channel: Channel) : IAction {\n    override fun execute(context: Context) {\n        try {\n            val intent = Intent(context, BrazeFeedActivity::class.java)\n            if (extras != null) {\n                intent.putExtras(extras)\n            }\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"BrazeFeedActivity was not opened successfully.\" }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/UriAction.kt",
    "content": "package com.braze.ui.actions\n\nimport android.annotation.SuppressLint\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.annotation.VisibleForTesting\nimport com.braze.Constants\nimport com.braze.enums.Channel\nimport com.braze.IBrazeDeeplinkHandler\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.REMOTE_SCHEMES\nimport com.braze.support.isLocalUri\nimport com.braze.ui.BrazeDeeplinkHandler\nimport com.braze.ui.BrazeWebViewActivity\nimport com.braze.ui.actions.brazeactions.BrazeActionParser\nimport com.braze.ui.actions.brazeactions.BrazeActionParser.isBrazeActionUri\nimport com.braze.ui.support.getMainActivityIntent\nimport com.braze.ui.support.isActivityRegisteredInManifest\n\nopen class UriAction : IAction {\n    val extras: Bundle?\n    override val channel: Channel\n\n    /**\n     * @return the [Uri] that represents this [UriAction].\n     */\n    var uri: Uri\n\n    /**\n     * @return whether this [UriAction] should open\n     */\n    var useWebView: Boolean\n\n    /**\n     * @param uri        The Uri.\n     * @param extras     Any extras to be passed in the start intent.\n     * @param useWebView If this Uri should use the Webview, if the Uri is a remote Uri\n     * @param channel    The channel for the Uri. Must not be null.\n     */\n    constructor(uri: Uri, extras: Bundle?, useWebView: Boolean, channel: Channel) {\n        this.uri = uri\n        this.extras = extras\n        this.useWebView = useWebView\n        this.channel = channel\n    }\n\n    /**\n     * Constructor to copy an existing [UriAction].\n     *\n     * @param original A [UriAction] to copy parameters from.\n     */\n    constructor(original: UriAction) {\n        uri = original.uri\n        extras = original.extras\n        useWebView = original.useWebView\n        channel = original.channel\n    }\n\n    /**\n     * Opens the action's Uri properly based on useWebView status and channel.\n     */\n    override fun execute(context: Context) {\n        if (uri.isLocalUri()) {\n            brazelog { \"Not executing local Uri: $uri\" }\n            return\n        }\n        if (uri.isBrazeActionUri()) {\n            brazelog(V) { \"Executing BrazeActions uri:\\n'$uri'\" }\n            BrazeActionParser.execute(context, uri, channel)\n        } else {\n            brazelog { \"Executing Uri action from channel $channel: $uri. UseWebView: $useWebView. Extras: $extras\" }\n            if (useWebView && REMOTE_SCHEMES.contains(uri.scheme)) {\n                // If the scheme is not a remote scheme, we open it using an ACTION_VIEW intent.\n                if (channel == Channel.PUSH) {\n                    openUriWithWebViewActivityFromPush(context, uri, extras)\n                } else {\n                    openUriWithWebViewActivity(context, uri, extras)\n                }\n            } else {\n                if (channel == Channel.PUSH) {\n                    openUriWithActionViewFromPush(context, uri, extras)\n                } else {\n                    openUriWithActionView(context, uri, extras)\n                }\n            }\n        }\n    }\n\n    /**\n     * Opens the remote scheme Uri in [BrazeWebViewActivity].\n     */\n    protected fun openUriWithWebViewActivity(context: Context, uri: Uri, extras: Bundle?) {\n        val intent = getWebViewActivityIntent(context, uri, extras)\n        intent.flags = BrazeDeeplinkHandler.getInstance()\n            .getIntentFlags(IBrazeDeeplinkHandler.IntentFlagPurpose.URI_ACTION_OPEN_WITH_WEBVIEW_ACTIVITY)\n        try {\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"BrazeWebViewActivity not opened successfully.\" }\n        }\n    }\n\n    /**\n     * Uses an Intent.ACTION_VIEW intent to open the Uri.\n     */\n    protected open fun openUriWithActionView(context: Context, uri: Uri, extras: Bundle?) {\n        val intent = getActionViewIntent(context, uri, extras)\n        intent.flags = BrazeDeeplinkHandler.getInstance()\n            .getIntentFlags(IBrazeDeeplinkHandler.IntentFlagPurpose.URI_ACTION_OPEN_WITH_ACTION_VIEW)\n        try {\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to handle uri $uri with extras: $extras\" }\n        }\n    }\n\n    /**\n     * Opens the remote scheme Uri in [BrazeWebViewActivity] while also populating the back stack.\n     *\n     * @see [UriAction.getIntentArrayWithConfiguredBackStack]\n     */\n    protected fun openUriWithWebViewActivityFromPush(context: Context, uri: Uri, extras: Bundle?) {\n        val configurationProvider = BrazeConfigurationProvider(context)\n        try {\n            val webViewIntent = getWebViewActivityIntent(context, uri, extras)\n            context.startActivities(\n                getIntentArrayWithConfiguredBackStack(\n                    context,\n                    extras,\n                    webViewIntent,\n                    configurationProvider\n                )\n            )\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Braze WebView Activity not opened successfully.\" }\n        }\n    }\n\n    /**\n     * Uses an [Intent.ACTION_VIEW] intent to open the [Uri] and places the main activity of the\n     * activity on the back stack.\n     *\n     * @see [UriAction.getIntentArrayWithConfiguredBackStack]\n     */\n    protected fun openUriWithActionViewFromPush(context: Context, uri: Uri, extras: Bundle?) {\n        val configurationProvider = BrazeConfigurationProvider(context)\n        try {\n            val uriIntent = getActionViewIntent(context, uri, extras)\n            context.startActivities(\n                getIntentArrayWithConfiguredBackStack(\n                    context,\n                    extras,\n                    uriIntent,\n                    configurationProvider\n                )\n            )\n        } catch (e: ActivityNotFoundException) {\n            brazelog(W, e) { \"Could not find appropriate activity to open for deep link $uri\" }\n        }\n    }\n\n    /**\n     * Returns an intent that opens the uri inside of a [BrazeWebViewActivity].\n     */\n    protected fun getWebViewActivityIntent(context: Context, uri: Uri, extras: Bundle?): Intent {\n        val configurationProvider = BrazeConfigurationProvider(context)\n        val customWebViewActivityClassName = configurationProvider.customHtmlWebViewActivityClassName\n\n        // If the class is valid and is manifest registered, use it as the launching intent\n        val webViewActivityIntent: Intent = if (!customWebViewActivityClassName.isNullOrBlank()\n            && isActivityRegisteredInManifest(\n                    context,\n                    customWebViewActivityClassName\n                )\n        ) {\n            brazelog { \"Launching custom WebView Activity with class name: $customWebViewActivityClassName\" }\n            Intent()\n                .setClassName(context, customWebViewActivityClassName)\n        } else {\n            Intent(context, BrazeWebViewActivity::class.java)\n        }\n        if (extras != null) {\n            webViewActivityIntent.putExtras(extras)\n        }\n        webViewActivityIntent.putExtra(Constants.BRAZE_WEBVIEW_URL_EXTRA, uri.toString())\n        return webViewActivityIntent\n    }\n\n    @SuppressLint(\"QueryPermissionsNeeded\")\n    protected fun getActionViewIntent(context: Context, uri: Uri, extras: Bundle?): Intent {\n        val intent = Intent(Intent.ACTION_VIEW)\n        intent.data = uri\n        if (extras != null) {\n            intent.putExtras(extras)\n        }\n\n        // If the current app can already handle the intent, default to using it\n        val resolveInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))\n        } else {\n            @Suppress(\"DEPRECATION\")\n            context.packageManager.queryIntentActivities(intent, 0)\n        }\n        if (resolveInfos.size > 1) {\n            for (resolveInfo in resolveInfos) {\n                if (resolveInfo.activityInfo.packageName == context.packageName) {\n                    brazelog { \"Setting deep link intent package to ${resolveInfo.activityInfo.packageName}.\" }\n                    intent.setPackage(resolveInfo.activityInfo.packageName)\n                    break\n                }\n            }\n        }\n        return intent\n    }\n\n    /**\n     * Gets an [Intent] array that has the configured back stack functionality.\n     *\n     * @param targetIntent The ultimate intent to be followed. For example, the main/launcher intent would be the penultimate [Intent].\n     * @see [BrazeConfigurationProvider.isPushDeepLinkBackStackActivityEnabled]\n     * @see [BrazeConfigurationProvider.pushDeepLinkBackStackActivityClassName]\n     */\n    @VisibleForTesting\n    @Suppress(\"NestedBlockDepth\")\n    fun getIntentArrayWithConfiguredBackStack(\n        context: Context,\n        extras: Bundle?,\n        targetIntent: Intent,\n        configurationProvider: BrazeConfigurationProvider\n    ): Array<Intent> {\n        // The root intent will either point to the launcher activity,\n        // some custom activity, or nothing if the back-stack is disabled.\n        var rootIntent: Intent? = null\n        if (configurationProvider.isPushDeepLinkBackStackActivityEnabled) {\n            // If a custom back stack class is defined, then set it\n            val activityClass = configurationProvider.pushDeepLinkBackStackActivityClassName\n            if (activityClass.isNullOrBlank()) {\n                brazelog(I) { \"Adding main activity intent to back stack while opening uri from push\" }\n                rootIntent = getMainActivityIntent(context, extras)\n            } else {\n                // Check if the activity is registered in the manifest. If not, then add nothing to the back stack\n                if (isActivityRegisteredInManifest(context, activityClass)) {\n                    brazelog(I) { \"Adding custom back stack activity while opening uri from push: $activityClass\" }\n                    rootIntent = extras?.let {\n                        Intent()\n                            .setClassName(context, activityClass)\n                            .setFlags(\n                                BrazeDeeplinkHandler.getInstance()\n                                    .getIntentFlags(IBrazeDeeplinkHandler.IntentFlagPurpose.URI_ACTION_BACK_STACK_GET_ROOT_INTENT)\n                            )\n                            .putExtras(it)\n                    }\n                } else {\n                    brazelog(I) { \"Not adding unregistered activity to the back stack while opening uri from push: $activityClass\" }\n                }\n            }\n        } else {\n            brazelog(I) { \"Not adding back stack activity while opening uri from push due to disabled configuration setting.\" }\n        }\n        return if (rootIntent == null) {\n            // Calling startActivities() from outside of an Activity\n            // context requires the FLAG_ACTIVITY_NEW_TASK flag on the first Intent\n            targetIntent.flags = BrazeDeeplinkHandler.getInstance()\n                .getIntentFlags(IBrazeDeeplinkHandler.IntentFlagPurpose.URI_ACTION_BACK_STACK_ONLY_GET_TARGET_INTENT)\n\n            // Just return the target intent by itself\n            arrayOf(targetIntent)\n        } else {\n            // Return the intents in their stack order\n            arrayOf(rootIntent, targetIntent)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/BrazeActionParser.kt",
    "content": "package com.braze.ui.actions.brazeactions\n\nimport android.content.Context\nimport android.net.Uri\nimport android.util.Base64\nimport com.braze.enums.Channel\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.getOptionalString\nimport com.braze.ui.actions.brazeactions.steps.AddToCustomAttributeArrayStep\nimport com.braze.ui.actions.brazeactions.steps.AddToSubscriptionGroupStep\nimport com.braze.ui.actions.brazeactions.steps.ContainerStep\nimport com.braze.ui.actions.brazeactions.steps.IBrazeActionStep\nimport com.braze.ui.actions.brazeactions.steps.LogCustomEventStep\nimport com.braze.ui.actions.brazeactions.steps.NoOpStep\nimport com.braze.ui.actions.brazeactions.steps.OpenLinkExternallyStep\nimport com.braze.ui.actions.brazeactions.steps.OpenLinkInWebViewStep\nimport com.braze.ui.actions.brazeactions.steps.RemoveFromCustomAttributeArrayStep\nimport com.braze.ui.actions.brazeactions.steps.RequestPushPermissionStep\nimport com.braze.ui.actions.brazeactions.steps.SetCustomUserAttributeStep\nimport com.braze.ui.actions.brazeactions.steps.SetEmailSubscriptionStep\nimport com.braze.ui.actions.brazeactions.steps.SetPushNotificationSubscriptionStep\nimport com.braze.ui.actions.brazeactions.steps.StepData\nimport org.json.JSONObject\n\nobject BrazeActionParser {\n    private const val BRAZE_ACTIONS_V1 = \"v1\"\n    internal const val TYPE = \"type\"\n    internal const val BRAZE_ACTIONS_SCHEME = \"brazeActions\"\n\n    internal enum class ActionType(val key: String, val impl: IBrazeActionStep) {\n        CONTAINER(\"container\", ContainerStep),\n        LOG_CUSTOM_EVENT(\"logCustomEvent\", LogCustomEventStep),\n        SET_CUSTOM_ATTRIBUTE(\"setCustomUserAttribute\", SetCustomUserAttributeStep),\n        REQUEST_PUSH_PERMISSION(\"requestPushPermission\", RequestPushPermissionStep),\n        ADD_TO_SUBSCRIPTION_GROUP(\"addToSubscriptionGroup\", AddToSubscriptionGroupStep),\n        REMOVE_FROM_SUBSCRIPTION_GROUP(\"removeFromSubscriptionGroup\", AddToSubscriptionGroupStep),\n        ADD_TO_CUSTOM_ATTRIBUTE_ARRAY(\"addToCustomAttributeArray\", AddToCustomAttributeArrayStep),\n        REMOVE_FROM_CUSTOM_ATTRIBUTE_ARRAY(\"removeFromCustomAttributeArray\", RemoveFromCustomAttributeArrayStep),\n        SET_EMAIL_SUBSCRIPTION(\"setEmailNotificationSubscriptionType\", SetEmailSubscriptionStep),\n        SET_PUSH_NOTIFICATION_SUBSCRIPTION(\"setPushNotificationSubscriptionType\", SetPushNotificationSubscriptionStep),\n        OPEN_LINK_IN_WEBVIEW(\"openLinkInWebview\", OpenLinkInWebViewStep),\n        OPEN_LINK_EXTERNALLY(\"openLink\", OpenLinkExternallyStep),\n        INVALID(\"\", NoOpStep);\n\n        companion object {\n            private val map = values().associateBy { it.key }\n\n            @JvmStatic\n            fun fromValue(value: String?): ActionType = map.getOrElse(value.orEmpty()) { INVALID }\n        }\n    }\n\n    fun Uri?.isBrazeActionUri(): Boolean = this?.scheme == BRAZE_ACTIONS_SCHEME\n\n    /**\n     * Parses a Braze Actions [Uri].\n     */\n    fun execute(context: Context, uri: Uri, channel: Channel = Channel.UNKNOWN) {\n        brazelog(V) { \"Attempting to parse Braze Action with channel $channel and uri:\\n'$uri'\" }\n        try {\n            val components = uri.getBrazeActionVersionAndJson()\n            if (components == null) {\n                brazelog(I) {\n                    \"Failed to decode Braze Action into both \" +\n                        \"version and json components. Doing nothing.\"\n                }\n                return\n            }\n\n            val (version, json) = components\n            if (version != BRAZE_ACTIONS_V1) {\n                brazelog {\n                    \"Braze Actions version $version is \" +\n                        \"unsupported. Version must be $BRAZE_ACTIONS_V1\"\n                }\n                return\n            }\n\n            parse(context, StepData(json, channel))\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to parse uri as a Braze Action.\\n'$uri'\" }\n        }\n        brazelog(V) { \"Done handling Braze uri\\n'$uri'\" }\n    }\n\n    /**\n     * Evaluates the Braze Action json for validity before running it.\n     */\n    @JvmSynthetic\n    internal fun parse(\n        context: Context,\n        data: StepData\n    ) {\n        try {\n            val actionType = getActionType(data)\n            if (actionType == ActionType.INVALID) {\n                return\n            }\n            brazelog(V) {\n                \"Performing Braze Action type $actionType with data $data\"\n            }\n            actionType.impl.run(context, data)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to run with data $data\" }\n        }\n    }\n\n    /**\n     * Parses an encoded URL-SAFE BASE64 input to back to UTF-16. Note\n     * that the base64 input is skip encoded into 8 bits due to\n     * fit Base64 (UTF-8).\n     */\n    @JvmSynthetic\n    internal fun parseEncodedActionToJson(action: String): JSONObject {\n        // Convert the base64 input into an array of 8-bit words\n        val bytes8: ByteArray = Base64.decode(action, Base64.URL_SAFE)\n\n        // Combine every pair of 8-bit words into a single 16-bit word\n        val bit16 = IntArray(bytes8.size / 2)\n        for (i in bytes8.indices step 2) {\n            @Suppress(\"MagicNumber\")\n            val lowerByte = bytes8[i].toInt() and 0xFF\n\n            @Suppress(\"UnnecessaryParentheses\", \"MagicNumber\")\n            val upperByte = (bytes8[i + 1].toInt() and 0xFF) shl 8\n            val combined = upperByte or lowerByte\n            bit16[i / 2] = combined\n        }\n\n        // Convert each 16-bit word into a UTF-16 character\n        // and combine each into a String\n        val result = StringBuilder()\n        for (code in bit16) {\n            result.append(Char(code))\n        }\n        return JSONObject(result.toString())\n    }\n\n    /**\n     * Extracts the version from the Braze Action [Uri] and\n     * decodes the Base64 portion of the action to [JSONObject].\n     * Assumes that the [Uri] is a valid Braze Action.\n     *\n     * @see isBrazeActionUri\n     */\n    @JvmSynthetic\n    internal fun Uri.getBrazeActionVersionAndJson(): Pair<String, JSONObject>? {\n        // Extract the version and encoded action\n        val version = this.host\n        val encodedAction = this.lastPathSegment\n\n        if (version == null || encodedAction == null) {\n            brazelog { \"Failed to parse version and encoded action from uri: $this\" }\n            return null\n        }\n\n        val json = try {\n            parseEncodedActionToJson(encodedAction)\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to decode action into json. Action:\\n'$encodedAction'\" }\n            null\n        } ?: return null\n\n        return Pair(version, json)\n    }\n\n    /**\n     * Parses the data for the step type and checks validity.\n     *\n     * @return The parsed [ActionType] or [ActionType.INVALID]\n     * if the [IBrazeActionStep.isValid] returns false.\n     */\n    @JvmSynthetic\n    internal fun getActionType(data: StepData): ActionType {\n        val type = ActionType.fromValue(data.srcJson.getOptionalString(TYPE))\n        val isValid = type.impl.isValid(data)\n        if (!isValid) {\n            brazelog {\n                \"Cannot parse invalid action of \" +\n                    \"type $type and data $data\"\n            }\n            return ActionType.INVALID\n        }\n        return type\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/BrazeActionUtils.kt",
    "content": "@file:JvmName(\"BrazeActionUtils\")\n\npackage com.braze.ui.actions.brazeactions\n\nimport android.net.Uri\nimport com.braze.models.cards.Card\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.ui.actions.brazeactions.BrazeActionParser.ActionType\nimport com.braze.ui.actions.brazeactions.BrazeActionParser.getBrazeActionVersionAndJson\nimport com.braze.ui.actions.brazeactions.BrazeActionParser.isBrazeActionUri\nimport com.braze.ui.actions.brazeactions.steps.ContainerStep\nimport com.braze.ui.actions.brazeactions.steps.StepData\nimport org.json.JSONObject\n\n/**\n * Determines whether any [Uri] in the message and/or buttons\n * contains a push prompt Braze Action.\n */\ninternal fun IInAppMessage.containsAnyPushPermissionBrazeActions(): Boolean =\n    doAnyTypesMatch(ActionType.REQUEST_PUSH_PERMISSION, this.getAllUris())\n\n/**\n * Determines whether any [Uri] in the message and/or buttons\n * contains an invalid action.\n */\ninternal fun IInAppMessage.containsInvalidBrazeAction() =\n    doAnyTypesMatch(ActionType.INVALID, this.getAllUris())\n\n/**\n * Determines whether any [Uri] in the card contains an invalid action.\n */\ninternal fun Card.containsInvalidBrazeAction(): Boolean {\n    if (this.url != null) {\n        return doAnyTypesMatch(ActionType.INVALID, listOf(Uri.parse(this.url)))\n    }\n    return false\n}\n\n/**\n * Retrieves all [Uri]'s from the main\n * message and any [MessageButton]'s present. Does not traverse any\n * [Uri] present in HTML messages.\n */\n@JvmSynthetic\ninternal fun IInAppMessage?.getAllUris(): List<Uri> {\n    if (this == null) return emptyList()\n    val uris = mutableListOf<Uri>()\n\n    // Add the main uri\n    this.uri?.let { uris.add(it) }\n\n    // Add all of the message button Uris\n    if (this is IInAppMessageImmersive) {\n        uris.addAll(\n            this.messageButtons.mapNotNull { it.uri }\n        )\n    }\n    return uris\n}\n\n@JvmSynthetic\ninternal fun getAllBrazeActionStepTypes(json: JSONObject): List<ActionType> {\n    val allStepTypes = mutableListOf<ActionType>()\n    val stepData = StepData(json)\n    when (val actionType = BrazeActionParser.getActionType(stepData)) {\n        // Break out the container steps and iterate them all\n        ActionType.CONTAINER -> {\n            ContainerStep.getChildStepIterator(stepData)\n                .forEach { allStepTypes.addAll(getAllBrazeActionStepTypes(it)) }\n        }\n\n        // This is an individual/uncontained step\n        else -> allStepTypes.add(actionType)\n    }\n    return allStepTypes\n}\n\n/**\n * Determines if any of the [Uri] in the list contain the given [ActionType].\n */\ninternal fun doAnyTypesMatch(actionType: ActionType, uriList: List<Uri>): Boolean =\n    uriList\n        .filter { it.isBrazeActionUri() }\n        // If the parsed json is null, then return an empty json\n        // object so we can return it as invalid in\n        // the next step.\n        .map { it.getBrazeActionVersionAndJson()?.second ?: JSONObject() }\n        .flatMap { getAllBrazeActionStepTypes(it) }\n        .any { it == actionType }\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/AddToCustomAttributeArrayStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\n\ninternal object AddToCustomAttributeArrayStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 2)\n            && isArgString(0)\n            && isArgString(1)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val key = data.firstArg.toString()\n        val value = data.secondArg.toString()\n        Braze.getInstance(context).runOnUser {\n            it.addToCustomAttributeArray(key, value)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/AddToSubscriptionGroupStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\n\ninternal object AddToSubscriptionGroupStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 1)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val subscriptionGroupId = data.firstArg.toString()\n        Braze.getInstance(context).runOnUser {\n            it.addToSubscriptionGroup(subscriptionGroupId)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/BaseBrazeActionStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport com.braze.Braze\nimport com.braze.BrazeUser\nimport com.braze.events.IValueCallback\nimport com.braze.support.BrazeLogger.brazelog\n\nsealed class BaseBrazeActionStep : IBrazeActionStep {\n    companion object {\n        internal fun Braze.runOnUser(block: (user: BrazeUser) -> Unit) {\n            this.getCurrentUser(object : IValueCallback<BrazeUser> {\n                override fun onSuccess(value: BrazeUser) {\n                    block(value)\n                }\n\n                override fun onError() {\n                    brazelog { \"Failed to run on Braze user object\" }\n                }\n            })\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/ContainerStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.support.iterator\nimport com.braze.ui.actions.brazeactions.BrazeActionParser\nimport org.json.JSONObject\n\ninternal object ContainerStep : BaseBrazeActionStep() {\n    internal const val STEPS = \"steps\"\n\n    override fun isValid(data: StepData): Boolean = data.srcJson.has(STEPS)\n\n    /**\n     * Container steps contain other steps under a special json array \"steps\".\n     */\n    override fun run(context: Context, data: StepData) {\n        for (step in getChildStepIterator(data)) {\n            // Each step is an action to parse\n            BrazeActionParser.parse(context, data.copy(srcJson = step))\n        }\n    }\n\n    @JvmSynthetic\n    internal fun getChildStepIterator(data: StepData): Iterator<JSONObject> =\n        data.srcJson.getJSONArray(STEPS).iterator()\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/IBrazeActionStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\n\ninternal sealed interface IBrazeActionStep {\n    /**\n     * Performs a best-effort check on any arguments\n     * to assert validity before step execution.\n     */\n    fun isValid(data: StepData): Boolean\n\n    /**\n     * Executes the Braze Action step.\n     */\n    fun run(\n        context: Context,\n        data: StepData\n    )\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/LogCustomEventStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\n\ninternal object LogCustomEventStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(rangedArgCount = 1..2)\n            && isArgString(0)\n            && isArgOptionalJsonObject(1)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        Braze.getInstance(context)\n            .logCustomEvent(\n                data.firstArg.toString(),\n                data.coerceArgToPropertiesOrNull(1)\n            )\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/NoOpStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\n\ninternal object NoOpStep : IBrazeActionStep {\n    override fun isValid(data: StepData): Boolean = false\n\n    override fun run(context: Context, data: StepData) {}\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/OpenLinkExternallyStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.ui.BrazeDeeplinkHandler\n\ninternal object OpenLinkExternallyStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        // The second parameter \"openInNewTab\" is not\n        // used on Android, but may be present. It is\n        // not checked for validity.\n        isArgCountInBounds(rangedArgCount = 1..2)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val url = data.firstArg.toString()\n        val handler = BrazeDeeplinkHandler.getInstance()\n        handler.createUriActionFromUrlString(\n            url = url,\n            extras = null,\n            openInWebView = false,\n            channel = data.channel\n        )?.let {\n            handler.gotoUri(context, it)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/OpenLinkInWebViewStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.ui.BrazeDeeplinkHandler\n\ninternal object OpenLinkInWebViewStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 1)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val url = data.firstArg.toString()\n        val handler = BrazeDeeplinkHandler.getInstance()\n        handler.createUriActionFromUrlString(\n            url = url,\n            extras = null,\n            openInWebView = true,\n            channel = data.channel\n        )?.let {\n            handler.gotoUri(context, it)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/RemoveFromCustomAttributeArrayStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\n\ninternal object RemoveFromCustomAttributeArrayStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 2)\n            && isArgString(0)\n            && isArgString(1)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        Braze.getInstance(context).runOnUser {\n            it.removeFromCustomAttributeArray(\n                key = data.firstArg.toString(),\n                value = data.secondArg.toString()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/RemoveFromSubscriptionGroupStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\n\ninternal object RemoveFromSubscriptionGroupStep : BaseBrazeActionStep() {\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 1)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val subscriptionGroupId = data.firstArg.toString()\n        Braze.getInstance(context).runOnUser {\n            it.removeFromSubscriptionGroup(subscriptionGroupId)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/RequestPushPermissionStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.support.requestPushPermissionPrompt\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\n\ninternal object RequestPushPermissionStep : BaseBrazeActionStep() {\n    /**\n     * This step does not require any arguments.\n     */\n    override fun isValid(data: StepData): Boolean = true\n\n    override fun run(context: Context, data: StepData) {\n        BrazeInAppMessageManager.getInstance().activity.requestPushPermissionPrompt()\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/SetCustomUserAttributeStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.Braze\nimport com.braze.support.BrazeLogger.brazeLogTag\n\ninternal object SetCustomUserAttributeStep : BaseBrazeActionStep() {\n    val TAG = SetCustomUserAttributeStep.brazeLogTag()\n\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 2)\n            && isArgString(0)\n            && secondArg != null\n    }\n\n    override fun run(context: Context, data: StepData) {\n        // This value was already checked\n        // for nullity in `isValid()`\n        val value = data.secondArg ?: return\n        Braze.getInstance(context).runOnUser {\n            it.setCustomAttribute(\n                key = data.firstArg.toString(),\n                value\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/SetEmailSubscriptionStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.enums.NotificationSubscriptionType\nimport com.braze.Braze\nimport com.braze.support.BrazeLogger.brazeLogTag\nimport com.braze.support.BrazeLogger.brazelog\n\ninternal object SetEmailSubscriptionStep : BaseBrazeActionStep() {\n    val TAG = SetEmailSubscriptionStep.brazeLogTag()\n\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 1)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val subscriptionType = NotificationSubscriptionType.fromValue(data.firstArg.toString())\n        if (subscriptionType == null) {\n            brazelog { \"Could not parse subscription type from data: $data\" }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setEmailNotificationSubscriptionType(subscriptionType)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/SetPushNotificationSubscriptionStep.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport android.content.Context\nimport com.braze.enums.NotificationSubscriptionType\nimport com.braze.Braze\nimport com.braze.support.BrazeLogger.brazeLogTag\nimport com.braze.support.BrazeLogger.brazelog\n\ninternal object SetPushNotificationSubscriptionStep : BaseBrazeActionStep() {\n    val TAG = SetPushNotificationSubscriptionStep.brazeLogTag()\n\n    override fun isValid(data: StepData): Boolean = data.run {\n        isArgCountInBounds(fixedArgCount = 1)\n            && isArgString(0)\n    }\n\n    override fun run(context: Context, data: StepData) {\n        val subscriptionType = NotificationSubscriptionType.fromValue(data.firstArg.toString())\n        if (subscriptionType == null) {\n            brazelog { \"Could not parse subscription type from data $data\" }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setPushNotificationSubscriptionType(subscriptionType)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/actions/brazeactions/steps/StepData.kt",
    "content": "package com.braze.ui.actions.brazeactions.steps\n\nimport androidx.annotation.VisibleForTesting\nimport com.braze.enums.Channel\nimport com.braze.models.outgoing.BrazeProperties\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.getPrettyPrintedString\nimport com.braze.support.iterator\nimport org.json.JSONObject\n\n/**\n * A data object for retrieving information on the enclosed\n * [IBrazeActionStep] in its [JSONObject] form.\n */\ninternal data class StepData(\n    val srcJson: JSONObject,\n    val channel: Channel = Channel.UNKNOWN\n) {\n    private val args: List<Any> by lazy {\n        srcJson.optJSONArray(ARGS)\n            .iterator<Any>()\n            .asSequence()\n            .toList()\n    }\n\n    val firstArg by lazy { getArg(0) }\n    val secondArg by lazy { getArg(1) }\n\n    @VisibleForTesting\n    internal fun getArg(index: Int): Any? = args.getOrNull(index)\n\n    /**\n     * @return A [BrazeProperties] object of the argument [JSONObject] or null if\n     * that [JSONObject] cannot be retrieved.\n     */\n    fun coerceArgToPropertiesOrNull(index: Int): BrazeProperties? {\n        val props = args.getOrNull(index)\n        return if (props != null && props is JSONObject) {\n            BrazeProperties(props)\n        } else {\n            null\n        }\n    }\n\n    /**\n     * @return True if the number of arguments is within either\n     * the fixed or variable length bounds specified.\n     */\n    fun isArgCountInBounds(\n        fixedArgCount: Int = -1,\n        rangedArgCount: IntRange? = null,\n    ): Boolean {\n        if (fixedArgCount != -1) {\n            if (args.size != fixedArgCount) {\n                brazelog { \"Expected $fixedArgCount arguments. Got: $args\" }\n                return false\n            }\n        }\n        if (rangedArgCount != null) {\n            if (args.size !in rangedArgCount) {\n                brazelog { \"Expected $rangedArgCount arguments. Got: $args\" }\n                return false\n            }\n        }\n        return true\n    }\n\n    fun isArgString(index: Int): Boolean {\n        return if (getArg(index) is String) {\n            true\n        } else {\n            brazelog { \"Argument [$index] is not a String. Source: $srcJson\" }\n            false\n        }\n    }\n\n    /**\n     * @return True if the argument is either null or [JSONObject].\n     */\n    fun isArgOptionalJsonObject(index: Int): Boolean {\n        val arg = getArg(index)\n        return if (arg == null || arg is JSONObject) {\n            true\n        } else {\n            brazelog { \"Argument [$index] is not a JSONObject. Source: $srcJson\" }\n            false\n        }\n    }\n\n    override fun toString(): String = \"Channel $channel and json\\n${srcJson.getPrettyPrintedString()}\"\n\n    companion object {\n        internal const val ARGS = \"args\"\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/activities/BrazeBaseFragmentActivity.kt",
    "content": "package com.braze.ui.activities\n\nimport androidx.fragment.app.FragmentActivity\nimport com.braze.Braze\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\n\n/**\n * The BrazeBaseFragmentActivity class is a base class that includes the necessary Braze method\n * calls for basic analytics and in-app message integration.\n */\nopen class BrazeBaseFragmentActivity : FragmentActivity() {\n    public override fun onStart() {\n        super.onStart()\n        // Opens (or reopens) a Braze session.\n        // Note: This must be called in the onStart lifecycle method of EVERY Activity. Failure to do so\n        // will result in incomplete and/or erroneous analytics.\n        Braze.getInstance(this).openSession(this)\n    }\n\n    public override fun onResume() {\n        super.onResume()\n        // Registers the BrazeInAppMessageManager for the current Activity. This Activity will now listen for\n        // in-app messages from Braze.\n        BrazeInAppMessageManager.getInstance().registerInAppMessageManager(this)\n    }\n\n    public override fun onPause() {\n        super.onPause()\n        // Unregisters the BrazeInAppMessageManager.\n        BrazeInAppMessageManager.getInstance().unregisterInAppMessageManager(this)\n    }\n\n    public override fun onStop() {\n        super.onStop()\n        // Closes the current Braze session.\n        // Note: This must be called in the onStop lifecycle method of EVERY Activity. Failure to do so\n        // will result in incomplete and/or erroneous analytics.\n        Braze.getInstance(this).closeSession(this)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/activities/BrazeFeedActivity.java",
    "content": "package com.braze.ui.activities;\n\nimport android.os.Bundle;\n\nimport com.braze.ui.R;\n\n/**\n * The BrazeFeedActivity in an Activity class that displays the Braze News Feed Fragment. This\n * class can be used to integrate the Braze News Feed as an Activity.\n *\n * Note: To integrate the Braze News Feed as a Fragment instead of an Activity, use the\n * {@link BrazeFeedFragment} class.\n */\npublic class BrazeFeedActivity extends BrazeBaseFragmentActivity {\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.com_braze_feed_activity);\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/activities/ContentCardsActivity.kt",
    "content": "package com.braze.ui.activities\n\nimport android.os.Bundle\nimport com.braze.ui.R\n\n/**\n * The [ContentCardsActivity] in an Activity class that displays the Braze Content Cards\n * Fragment. This class can be used to integrate Content Cards as an Activity.\n *\n * Note: To integrate Braze Content Cards as a Fragment instead of an Activity, use the\n * [ContentCardsFragment] class.\n */\nopen class ContentCardsActivity : BrazeBaseFragmentActivity() {\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.com_braze_content_cards_activity)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/adapters/BrazeListAdapter.java",
    "content": "package com.braze.ui.adapters;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\n\nimport androidx.annotation.NonNull;\n\nimport com.braze.models.cards.BannerImageCard;\nimport com.braze.models.cards.CaptionedImageCard;\nimport com.braze.models.cards.Card;\nimport com.braze.models.cards.ShortNewsCard;\nimport com.braze.models.cards.TextAnnouncementCard;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.ui.widget.BannerImageCardView;\nimport com.braze.ui.widget.CaptionedImageCardView;\nimport com.braze.ui.widget.DefaultCardView;\nimport com.braze.ui.widget.ShortNewsCardView;\nimport com.braze.ui.widget.TextAnnouncementCardView;\nimport com.braze.support.BrazeLogger;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Default adapter used to display cards and log card impressions for the Braze news feed.\n * <p/>\n * This allows the stream to reuse cards when they go out of view.\n * <p/>\n * IMPORTANT - When you add a new card, be sure to add the new view type and update the view count here\n * <p/>\n * A card generates an impression once per viewing per open ListView. If a card is viewed more than once\n * in a particular ListView, it generates only one impression. If closed an reopened, a card will again\n * generate an impression. This also takes into account the case of a card being off-screen in the ListView.\n * The card only generates an impression when it actually scrolls onto the screen.\n * <p/>\n * IMPORTANT - You must call resetCardImpressionTracker() whenever the ListView is displayed. This will ensure\n * that cards that come into view will be tracked according to the description above.\n * <p/>\n * Adding and removing cards to and from the adapter should be done using the following synchronized\n * methods: {@link com.braze.ui.adapters.BrazeListAdapter#add(Card)},\n * {@link com.braze.ui.adapters.BrazeListAdapter#clear()}clear(),\n * {@link com.braze.ui.adapters.BrazeListAdapter#replaceFeed(java.util.List)}\n */\npublic class BrazeListAdapter extends ArrayAdapter<Card> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BrazeListAdapter.class);\n\n  private final Context mContext;\n  private final Set<String> mCardIdImpressions;\n\n  public BrazeListAdapter(Context context, int layoutResourceId, List<Card> cards) {\n    super(context, layoutResourceId, cards);\n    mContext = context;\n    mCardIdImpressions = new HashSet<>();\n  }\n\n  /**\n   * Be sure to keep view count in sync with the number of card types in the stream.\n   * It is used internally to return the correct card type and to cache views for reuse\n   */\n  @Override\n  public int getViewTypeCount() {\n    return 5;\n  }\n\n  @Override\n  public int getItemViewType(int position) {\n    Card card = getItem(position);\n    if (card instanceof BannerImageCard) {\n      return 1;\n    } else if (card instanceof CaptionedImageCard) {\n      return 2;\n    } else if (card instanceof ShortNewsCard) {\n      return 3;\n    } else if (card instanceof TextAnnouncementCard) {\n      return 4;\n    } else {\n      return 0;\n    }\n  }\n\n  /**\n   * Always try to use a convert view if possible, otherwise create one from scratch. The convertView should always\n   * be of the appropriate type, but it will be recycled, so you need to fully re-populate it with data from the card.\n   */\n  @NonNull\n  @Override\n  public View getView(int position, View convertView, @NonNull ViewGroup parent) {\n    BaseFeedCardView view;\n    Card card = getItem(position);\n\n    if (convertView == null) {\n      if (card instanceof BannerImageCard) {\n        view = new BannerImageCardView(mContext);\n      } else if (card instanceof CaptionedImageCard) {\n        view = new CaptionedImageCardView(mContext);\n      } else if (card instanceof ShortNewsCard) {\n        view = new ShortNewsCardView(mContext);\n      } else if (card instanceof TextAnnouncementCard) {\n        view = new TextAnnouncementCardView(mContext);\n      } else {\n        view = new DefaultCardView(mContext);\n      }\n    } else {\n      BrazeLogger.v(TAG, \"Reusing convertView for rendering of item \" + position);\n      view = (BaseFeedCardView) convertView;\n    }\n\n    BrazeLogger.v(TAG, \"Using view of type: \" + view.getClass().getName() + \" for card at position \" + position + \": \" + card.toString());\n    view.setCard(card);\n    logCardImpression(card);\n    return view;\n  }\n\n  @SuppressWarnings(\"checkstyle:localvariablename\")\n  public synchronized void replaceFeed(List<Card> cards) {\n    setNotifyOnChange(false);\n\n    if (cards == null) {\n      clear();\n      notifyDataSetChanged();\n      return;\n    }\n\n    BrazeLogger.d(TAG, \"Replacing existing feed of \" + getCount() + \" cards with new feed containing \" + cards.size() + \" cards.\");\n    int i = 0;\n    int j = 0;\n    int newFeedSize = cards.size();\n    Card existingCard;\n    Card newCard;\n\n    // Iterate over the entire existing feed, skipping items at the head of the list whenever they're the same as the\n    // head of the new list and otherwise removing them.\n    while (i < getCount()) {\n      existingCard = getItem(i);\n      newCard = null;\n\n      // Only consider a new card if there are any left.\n      if (j < newFeedSize) {\n        newCard = cards.get(j);\n      }\n\n      // If there is still a card to add and it is the same as the next existing card in the feed, continue.\n      if (newCard != null && newCard.equals(existingCard)) {\n        i++;\n        j++;\n      } else { // Otherwise, we need to get rid of the next card in the adapter, and continue checking.\n        remove(existingCard);\n      }\n    }\n\n    // Now we add the remainder of the feed.\n    super.addAll(cards.subList(j, newFeedSize));\n    notifyDataSetChanged();\n  }\n\n  @Override\n  public synchronized void add(Card card) {\n    super.add(card);\n  }\n\n  /**\n   * Resets the list of viewed cards. This must be called every time the ListView is displayed and it will reset\n   * the impressions.\n   */\n  public void resetCardImpressionTracker() {\n    mCardIdImpressions.clear();\n  }\n\n  private void logCardImpression(Card card) {\n    String cardId = card.getId();\n    if (!mCardIdImpressions.contains(cardId)) {\n      mCardIdImpressions.add(cardId);\n      card.logImpression();\n      BrazeLogger.v(TAG, \"Logged impression for card \" + cardId);\n    } else {\n      BrazeLogger.v(TAG, \"Already counted impression for card \" + cardId);\n    }\n    if (!card.getViewed()) {\n      card.setViewed(true);\n    }\n  }\n\n  /**\n   * Helper method to batch set cards to visually read after either an up or down scroll of the feed.\n   * Since scrolls can have multiple cards scrolled off screen at a time, this method can batch set those\n   * cards to read.\n   *\n   * @param startIndex Where to start setting cards to viewed. The card at this index will\n   *                   be set to viewed. Must be less than endIndex\n   * @param endIndex   Where to end setting cards to viewed. The card at this index will be set to viewed.\n   */\n  public void batchSetCardsToRead(int startIndex, int endIndex) {\n    if (getCount() == 0) {\n      BrazeLogger.d(TAG, \"mAdapter is empty in setting some cards to viewed.\");\n      return;\n    }\n\n    // Make sure the start and end are in bounds\n    startIndex = Math.max(0, startIndex);\n    endIndex = Math.min(getCount(), endIndex);\n\n    for (int traversalIndex = startIndex; traversalIndex < endIndex; traversalIndex++) {\n      // Get the card\n      Card card = getItem(traversalIndex);\n      if (card == null) {\n        BrazeLogger.d(TAG, \"Card was null in setting some cards to viewed.\");\n        break;\n      }\n\n      if (!card.isIndicatorHighlighted()) {\n        card.setIndicatorHighlighted(true);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/ContentCardsFragment.kt",
    "content": "package com.braze.ui.contentcards\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.SimpleItemAnimator\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout\nimport com.braze.ui.R\nimport com.braze.Braze\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.events.ContentCardsUpdatedEvent\nimport com.braze.events.ContentCardsUpdatedEvent.Companion.emptyUpdate\nimport com.braze.events.IEventSubscriber\nimport com.braze.events.SdkDataWipeEvent\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.contentcards.adapters.ContentCardAdapter\nimport com.braze.ui.contentcards.adapters.EmptyContentCardsAdapter\nimport com.braze.ui.contentcards.handlers.DefaultContentCardsUpdateHandler\nimport com.braze.ui.contentcards.handlers.DefaultContentCardsViewBindingHandler\nimport com.braze.ui.contentcards.handlers.IContentCardsUpdateHandler\nimport com.braze.ui.contentcards.handlers.IContentCardsViewBindingHandler\nimport com.braze.ui.contentcards.recycler.ContentCardsDividerItemDecoration\nimport com.braze.ui.contentcards.recycler.SimpleItemTouchHelperCallback\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\n\n/**\n * A fragment to display Braze ContentCards.\n */\n@Suppress(\"TooManyFunctions\")\nopen class ContentCardsFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {\n    /**\n     * A runnable to execute when the network is determined\n     * to be unavailable.\n     */\n    protected var networkUnavailableJob: Job? = null\n\n    /**\n     * A [RecyclerView] associated with [ContentCardsFragment].\n     * Note that this will be null until [Fragment.onCreateView] is called.\n     */\n    var contentCardsRecyclerView: RecyclerView? = null\n        protected set\n\n    @JvmField\n    var cardAdapter: ContentCardAdapter? = null\n\n    protected var defaultEmptyContentCardsAdapter: EmptyContentCardsAdapter = EmptyContentCardsAdapter()\n    protected var contentCardsSwipeLayout: SwipeRefreshLayout? = null\n\n    protected var contentCardsUpdatedSubscriber: IEventSubscriber<ContentCardsUpdatedEvent>? = null\n    protected var sdkDataWipeEventSubscriber: IEventSubscriber<SdkDataWipeEvent>? = null\n\n    protected val defaultContentCardUpdateHandler: IContentCardsUpdateHandler = DefaultContentCardsUpdateHandler()\n    protected var customContentCardUpdateHandler: IContentCardsUpdateHandler? = null\n    protected val defaultContentCardsViewBindingHandler: IContentCardsViewBindingHandler = DefaultContentCardsViewBindingHandler()\n    protected var customContentCardsViewBindingHandler: IContentCardsViewBindingHandler? = null\n\n    /**\n     * An adapter to display when no cards are available for display.\n     */\n    protected val emptyCardsAdapter: RecyclerView.Adapter<*>\n        get() = defaultEmptyContentCardsAdapter\n\n    // Since the get always returns non-null, but we want to allow passing null into the setter to\n    // clear the instance, we split this out into a custom getter and setter rather than using a var\n    /**\n     * @return the [IContentCardsUpdateHandler] for this [ContentCardsFragment].\n     */\n    fun getContentCardUpdateHandler() =\n        customContentCardUpdateHandler ?: defaultContentCardUpdateHandler\n\n    /**\n     * Set the [IContentCardsUpdateHandler] for this [ContentCardsFragment]. This handler is for doing\n     * any work on [Card]s before being rendered in the ContentCards.\n     * If not set or set to null, the default handler will be used.\n     */\n    fun setContentCardUpdateHandler(value: IContentCardsUpdateHandler?) {\n        customContentCardUpdateHandler = value\n    }\n\n    // Since the get always returns non-null, but we want to allow passing null into the setter to\n    // clear the instance, we split this out into a custom getter and setter rather than using a var\n    /**\n     * @return the [IContentCardsViewBindingHandler] responsible for rendering each [Card] in the [RecyclerView].\n     */\n    fun getContentCardsViewBindingHandler() =\n        customContentCardsViewBindingHandler ?: defaultContentCardsViewBindingHandler\n\n    /**\n     * Set the [IContentCardsViewBindingHandler] responsible for rendering each [Card] in the [RecyclerView].\n     * Note that this should only be set before the [ContentCardsFragment] is first displayed or the\n     * [ContentCardAdapter] will not update correctly.\n     */\n    fun setContentCardsViewBindingHandler(value: IContentCardsViewBindingHandler?) {\n        customContentCardsViewBindingHandler = value\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        val rootView = inflater.inflate(R.layout.com_braze_content_cards, container, false)\n        contentCardsRecyclerView = rootView.findViewById(R.id.com_braze_content_cards_recycler)\n        contentCardsSwipeLayout = rootView.findViewById(R.id.braze_content_cards_swipe_container)\n        contentCardsSwipeLayout?.setOnRefreshListener(this)\n        contentCardsSwipeLayout?.setColorSchemeResources(\n            R.color.com_braze_content_cards_swipe_refresh_color_1,\n            R.color.com_braze_content_cards_swipe_refresh_color_2,\n            R.color.com_braze_content_cards_swipe_refresh_color_3,\n            R.color.com_braze_content_cards_swipe_refresh_color_4\n        )\n        return rootView\n    }\n\n    /**\n     * Called when the user swipes down and requests a feed refresh.\n     */\n    override fun onRefresh() {\n        Braze.getInstance(requireContext()).requestContentCardsRefresh(false)\n        BrazeCoroutineScope.launchDelayed(AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS) { contentCardsSwipeLayout?.isRefreshing = false }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        // Remove the previous subscriber before rebuilding a new one with our new activity.\n        Braze.getInstance(requireContext()).removeSingleSubscription(contentCardsUpdatedSubscriber, ContentCardsUpdatedEvent::class.java)\n        if (contentCardsUpdatedSubscriber == null) {\n            contentCardsUpdatedSubscriber = IEventSubscriber { event: ContentCardsUpdatedEvent -> handleContentCardsUpdatedEvent(event) }\n        }\n        contentCardsUpdatedSubscriber?.let {\n            Braze.getInstance(requireContext()).subscribeToContentCardsUpdates(it)\n        }\n        Braze.getInstance(requireContext()).requestContentCardsRefresh(true)\n        Braze.getInstance(requireContext()).removeSingleSubscription(sdkDataWipeEventSubscriber, SdkDataWipeEvent::class.java)\n        if (sdkDataWipeEventSubscriber == null) {\n            // If the SDK data is wiped, then we want to clear any cached Content Cards\n            sdkDataWipeEventSubscriber = IEventSubscriber { handleContentCardsUpdatedEvent(emptyUpdate) }\n        }\n        sdkDataWipeEventSubscriber?.let {\n            Braze.getInstance(requireContext()).addSingleSynchronousSubscription(it, SdkDataWipeEvent::class.java)\n        }\n    }\n\n    override fun onPause() {\n        super.onPause()\n        // If the view is going away, we don't care about updating it anymore. Remove the subscription immediately.\n        Braze.getInstance(requireContext()).removeSingleSubscription(contentCardsUpdatedSubscriber, ContentCardsUpdatedEvent::class.java)\n        Braze.getInstance(requireContext()).removeSingleSubscription(sdkDataWipeEventSubscriber, SdkDataWipeEvent::class.java)\n        networkUnavailableJob?.cancel()\n        networkUnavailableJob = null\n        cardAdapter?.markOnScreenCardsAsRead()\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        contentCardsRecyclerView?.layoutManager?.let {\n            outState.putParcelable(LAYOUT_MANAGER_SAVED_INSTANCE_STATE_KEY, it.onSaveInstanceState())\n        }\n        cardAdapter?.let {\n            outState.putStringArrayList(KNOWN_CARD_IMPRESSIONS_SAVED_INSTANCE_STATE_KEY, ArrayList(it.impressedCardIds))\n        }\n        customContentCardsViewBindingHandler?.let {\n            outState.putParcelable(VIEW_BINDING_HANDLER_SAVED_INSTANCE_STATE_KEY, it)\n        }\n        customContentCardUpdateHandler?.let {\n            outState.putParcelable(UPDATE_HANDLER_SAVED_INSTANCE_STATE_KEY, it)\n        }\n    }\n\n    override fun onViewStateRestored(savedInstanceState: Bundle?) {\n        super.onViewStateRestored(savedInstanceState)\n        if (savedInstanceState != null) {\n            val updateHandlerParcelable: IContentCardsUpdateHandler? =\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    savedInstanceState.getParcelable(UPDATE_HANDLER_SAVED_INSTANCE_STATE_KEY, IContentCardsUpdateHandler::class.java)\n                } else {\n                    @Suppress(\"DEPRECATION\")\n                    savedInstanceState.getParcelable(UPDATE_HANDLER_SAVED_INSTANCE_STATE_KEY)\n                }\n            if (updateHandlerParcelable != null) {\n                setContentCardUpdateHandler(updateHandlerParcelable)\n            }\n            val viewBindingHandlerParcelable: IContentCardsViewBindingHandler? =\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    savedInstanceState.getParcelable(VIEW_BINDING_HANDLER_SAVED_INSTANCE_STATE_KEY, IContentCardsViewBindingHandler::class.java)\n                } else {\n                    @Suppress(\"DEPRECATION\")\n                    savedInstanceState.getParcelable(VIEW_BINDING_HANDLER_SAVED_INSTANCE_STATE_KEY)\n                }\n            if (viewBindingHandlerParcelable != null) {\n                setContentCardsViewBindingHandler(viewBindingHandlerParcelable)\n            }\n            BrazeCoroutineScope.launch(Dispatchers.Main) {\n                val layoutManagerState =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        savedInstanceState.getParcelable(LAYOUT_MANAGER_SAVED_INSTANCE_STATE_KEY, Parcelable::class.java)\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        savedInstanceState.getParcelable(LAYOUT_MANAGER_SAVED_INSTANCE_STATE_KEY)\n                    }\n                contentCardsRecyclerView?.let {\n                    val layoutManager = it.layoutManager\n                    if (layoutManagerState != null && layoutManager != null) {\n                        layoutManager.onRestoreInstanceState(layoutManagerState)\n                    }\n                }\n                cardAdapter?.let {\n                    val savedCardIdImpressions: List<String>? = savedInstanceState.getStringArrayList(KNOWN_CARD_IMPRESSIONS_SAVED_INSTANCE_STATE_KEY)\n                    if (savedCardIdImpressions != null) {\n                        it.impressedCardIds = savedCardIdImpressions\n                    }\n                }\n            }\n        }\n        initializeRecyclerView()\n    }\n\n    protected fun initializeRecyclerView() {\n        val layoutManager = LinearLayoutManager(activity)\n        cardAdapter = ContentCardAdapter(\n            requireContext(),\n            layoutManager,\n            mutableListOf(),\n            getContentCardsViewBindingHandler()\n        )\n        contentCardsRecyclerView?.adapter = cardAdapter\n        contentCardsRecyclerView?.layoutManager = layoutManager\n        attachSwipeHelperCallback()\n\n        // Disable any animations when the items change to avoid any issues when the data changes\n        // see https://stackoverflow.com/questions/29331075/recyclerview-blinking-after-notifydatasetchanged\n        val animator = contentCardsRecyclerView?.itemAnimator\n        if (animator is SimpleItemAnimator) {\n            animator.supportsChangeAnimations = false\n        }\n\n        // Add an item divider\n        contentCardsRecyclerView?.addItemDecoration(ContentCardsDividerItemDecoration(requireContext()))\n    }\n\n    /**\n     * Creates and attaches a [SimpleItemTouchHelperCallback] to handle swipe-to-dismiss functionality.\n     */\n    protected fun attachSwipeHelperCallback() {\n        cardAdapter?.let {\n            val itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(it))\n            itemTouchHelper.attachToRecyclerView(contentCardsRecyclerView)\n        }\n    }\n\n    /**\n     * Handles the processing and rendering for a [ContentCardsUpdatedEvent] on the UI thread.\n     */\n    protected fun handleContentCardsUpdatedEvent(event: ContentCardsUpdatedEvent) {\n        BrazeCoroutineScope.launch(Dispatchers.Main) { contentCardsUpdate(event) }\n    }\n\n    /**\n     * A main thread runnable to handle [ContentCardsUpdatedEvent] on the main thread.\n     */\n    protected suspend fun contentCardsUpdate(event: ContentCardsUpdatedEvent) {\n        brazelog(V) { \"Updating Content Cards views in response to ContentCardsUpdatedEvent: $event\" }\n        // This list of cards could undergo filtering in the card update handler\n        // and be a smaller list of cards compared to the original list\n        // in the update event. Thus, any \"empty feed\" checks should be\n        // performed on this filtered list and not the original list of cards.\n        val cardsForRendering = getContentCardUpdateHandler().handleCardUpdate(event)\n        cardAdapter?.replaceCards(cardsForRendering)\n        networkUnavailableJob?.cancel()\n        networkUnavailableJob = null\n\n        // If the update came from storage and is stale, then request a refresh.\n        if (event.isFromOfflineStorage && event.isTimestampOlderThan(MAX_CONTENT_CARDS_TTL_SECONDS.toLong())) {\n            brazelog(I) {\n                \"ContentCards received was older than the max time to live of \" +\n                    \"$MAX_CONTENT_CARDS_TTL_SECONDS seconds, displaying it for now, but \" +\n                    \"requesting an updated view from the server.\"\n            }\n            Braze.getInstance(requireContext()).requestContentCardsRefresh(false)\n\n            // If we don't have any cards to display, we put up the spinner while\n            // we wait for the network to return.\n            // Eventually displaying an error message if it doesn't.\n            if (cardsForRendering.isEmpty()) {\n                // Display a loading indicator\n                contentCardsSwipeLayout?.isRefreshing = true\n                brazelog {\n                    \"Old Content Cards was empty, putting up a network spinner and \" +\n                        \"registering the network error message on a delay of \" +\n                        \"$NETWORK_PROBLEM_WARNING_MS ms.\"\n                }\n\n                networkUnavailableJob?.cancel()\n                networkUnavailableJob = BrazeCoroutineScope.launchDelayed(NETWORK_PROBLEM_WARNING_MS, Dispatchers.Main) {\n                    networkUnavailable()\n                }\n                return\n            }\n        }\n\n        // The cards are either fresh from the cache, or came directly from a\n        // network request. An empty Content Cards should just display\n        // an \"empty ContentCards\" message.\n        if (cardsForRendering.isNotEmpty()) {\n            // The Content Cards contains cards and should be displayed.\n            // The card adapter shouldn't be null at this point\n            cardAdapter?.let {\n                swapRecyclerViewAdapter(it)\n            }\n        } else {\n            // The Content Cards is empty and should display an \"empty\" message to the user.\n            swapRecyclerViewAdapter(emptyCardsAdapter)\n        }\n\n        // Stop the refresh animation\n        contentCardsSwipeLayout?.isRefreshing = false\n    }\n\n    /**\n     * A main thread runnable to handle displaying network unavailable messages on the main thread.\n     */\n    protected suspend fun networkUnavailable() {\n        brazelog(V) { \"Displaying network unavailable toast.\" }\n        context?.applicationContext?.let { applicationContext ->\n            Toast.makeText(\n                applicationContext,\n                applicationContext.getString(R.string.com_braze_feed_connection_error_title),\n                Toast.LENGTH_LONG\n            ).show()\n        }\n        swapRecyclerViewAdapter(emptyCardsAdapter)\n        contentCardsSwipeLayout?.isRefreshing = false\n    }\n\n    /**\n     * Swaps the current [RecyclerView] [RecyclerView.Adapter] for a new one. If\n     * the current adapter matches the new adapter, then this method does nothing.\n     */\n    protected fun swapRecyclerViewAdapter(newAdapter: RecyclerView.Adapter<*>) {\n        val recyclerView = contentCardsRecyclerView\n        if (recyclerView != null && recyclerView.adapter !== newAdapter) {\n            recyclerView.adapter = newAdapter\n        }\n    }\n\n    companion object {\n        private const val MAX_CONTENT_CARDS_TTL_SECONDS = 60\n        private const val NETWORK_PROBLEM_WARNING_MS = 5000L\n        private const val AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS = 2500L\n        private const val LAYOUT_MANAGER_SAVED_INSTANCE_STATE_KEY = \"LAYOUT_MANAGER_SAVED_INSTANCE_STATE_KEY\"\n        private const val KNOWN_CARD_IMPRESSIONS_SAVED_INSTANCE_STATE_KEY = \"KNOWN_CARD_IMPRESSIONS_SAVED_INSTANCE_STATE_KEY\"\n        private const val VIEW_BINDING_HANDLER_SAVED_INSTANCE_STATE_KEY = \"VIEW_BINDING_HANDLER_SAVED_INSTANCE_STATE_KEY\"\n        private const val UPDATE_HANDLER_SAVED_INSTANCE_STATE_KEY = \"UPDATE_HANDLER_SAVED_INSTANCE_STATE_KEY\"\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/adapters/ContentCardAdapter.kt",
    "content": "package com.braze.ui.contentcards.adapters\n\nimport android.content.Context\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.ViewGroup\nimport androidx.annotation.VisibleForTesting\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.braze.models.cards.Card\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.contentcards.handlers.IContentCardsViewBindingHandler\nimport com.braze.ui.contentcards.managers.BrazeContentCardsManager\nimport com.braze.ui.contentcards.recycler.ItemTouchHelperAdapter\nimport com.braze.ui.contentcards.view.ContentCardViewHolder\nimport kotlin.math.max\nimport kotlin.math.min\n\n@Suppress(\"TooManyFunctions\")\nclass ContentCardAdapter(\n    private val context: Context,\n    private val layoutManager: LinearLayoutManager,\n    private val cardData: MutableList<Card>,\n    private val contentCardsViewBindingHandler: IContentCardsViewBindingHandler\n) : RecyclerView.Adapter<ContentCardViewHolder>(), ItemTouchHelperAdapter {\n\n    // Handler is still used here instead of coroutines because it guarantees order\n    private val handler: Handler = Handler(Looper.getMainLooper())\n    private var impressedCardIdsInternal = mutableSetOf<String>()\n\n    /**\n     * A list of the impressed card ids.\n     */\n    var impressedCardIds: List<String>\n        get() = impressedCardIdsInternal.toList()\n        set(impressedCardIds) {\n            impressedCardIdsInternal = impressedCardIds.toMutableSet()\n        }\n\n    init {\n        // We use stable ids to ensure that the same ViewHolder gets used with the same item.\n        setHasStableIds(true)\n    }\n\n    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int) =\n        contentCardsViewBindingHandler.onCreateViewHolder(context, cardData, viewGroup, viewType)\n\n    override fun onBindViewHolder(viewHolder: ContentCardViewHolder, position: Int) {\n        contentCardsViewBindingHandler.onBindViewHolder(context, cardData, viewHolder, position)\n    }\n\n    override fun getItemViewType(position: Int) =\n        contentCardsViewBindingHandler.getItemViewType(context, cardData, position)\n\n    override fun getItemCount() =\n        cardData.size\n\n    override fun onItemDismiss(position: Int) {\n        // Note that the ordering of these operations is important. We can't notify of item removal until the item has\n        // actually been removed. Additionally, we can only call the card action listener after the card removal\n        // as per the onContentCardDismissed() specification.\n        if (isInvalidIndex(position)) {\n            brazelog { \"Cannot dismiss card at index: $position in cards list of size: ${cardData.size}\" }\n            return\n        }\n        val removedCard = cardData.removeAt(position)\n        removedCard.isDismissed = true\n        notifyItemRemoved(position)\n        BrazeContentCardsManager.instance.contentCardsActionListener?.onContentCardDismissed(context, removedCard)\n    }\n\n    override fun isItemDismissable(position: Int): Boolean {\n        return if (cardData.isEmpty() || isInvalidIndex(position)) {\n            false\n        } else {\n            cardData[position].isDismissibleByUser\n        }\n    }\n\n    override fun onViewAttachedToWindow(holder: ContentCardViewHolder) {\n        // Note that onViewAttachedToWindow() is called right before a view is \"visible\".\n        // I.e. the Layout Manager has not yet updated its \"first/last visible item position\"\n        super.onViewAttachedToWindow(holder)\n\n        // If the cards are empty just return\n        if (cardData.isEmpty()) {\n            return\n        }\n        val adapterPosition = holder.bindingAdapterPosition\n        if (adapterPosition == RecyclerView.NO_POSITION || !isAdapterPositionOnScreen(\n                adapterPosition\n            )\n        ) {\n            brazelog(V) {\n                \"The card at position $adapterPosition isn't on screen or does not have a valid adapter position. Not logging impression.\"\n            }\n            return\n        }\n        logImpression(getCardAtIndex(adapterPosition))\n    }\n\n    override fun onViewDetachedFromWindow(holder: ContentCardViewHolder) {\n        // Note that onViewDetachedFromWindow() is called right before a view is technically \"non-visible\".\n        // I.e. once a view goes off-screen, it's first detached from the window, then the Layout Manager updates its \"first/last visible item position\"\n        // Also note that when the RecyclerView is paused, onViewDetachedFromWindow() is called for all attached views.\n        super.onViewDetachedFromWindow(holder)\n\n        // If the cards are empty just return\n        if (cardData.isEmpty()) {\n            return\n        }\n        val adapterPosition = holder.bindingAdapterPosition\n\n        // RecyclerView will attach some number of views above/below the visible views on screen.\n        // However, when onViewDetachedFromWindow() is called for each of those views, regardless of\n        // whether it was visible or not. We do not want to mistakenly mark some cards as\n        // \"read\" if the user never actually saw them. E.g. if views [A B C] were visible on\n        // screen, RecyclerView could have attached views ( A B C D E ).\n        // Without this check, we would mistakenly mark views D & E as read.\n        if (adapterPosition == RecyclerView.NO_POSITION ||\n            !isAdapterPositionOnScreen(adapterPosition)\n        ) {\n            brazelog(V) { \"The card at position $adapterPosition isn't on screen or does not have a valid adapter position. Not marking as read.\" }\n            return\n        }\n\n        // Get the card at this adapter position\n        // If the card is null, then there's nothing to notify or update\n        val cardAtPosition = getCardAtIndex(adapterPosition) ?: return\n\n        if (!cardAtPosition.isIndicatorHighlighted) {\n            cardAtPosition.isIndicatorHighlighted = true\n\n            // Mark as changed\n            handler.post { notifyItemChanged(adapterPosition) }\n        }\n    }\n\n    override fun getItemId(position: Int): Long {\n        val card = getCardAtIndex(position)\n        return card?.id?.hashCode()?.toLong() ?: 0\n    }\n\n    @Synchronized\n    fun replaceCards(newCardData: List<Card>) {\n        val diffCallback = CardListDiffCallback(cardData, newCardData)\n        val diffResult = DiffUtil.calculateDiff(diffCallback)\n        cardData.clear()\n        cardData.addAll(newCardData)\n\n        // The diff dispatch will call the adapter notify methods\n        diffResult.dispatchUpdatesTo(this)\n    }\n\n    /**\n     * Marks every on-screen card as read.\n     */\n    fun markOnScreenCardsAsRead() {\n        if (cardData.isEmpty()) {\n            brazelog { \"Card list is empty. Not marking on-screen cards as read.\" }\n            return\n        }\n        val firstVisibleIndex = layoutManager.findFirstVisibleItemPosition()\n        val lastVisibleIndex = layoutManager.findLastVisibleItemPosition()\n\n        // Either case could arise if there are no items in the adapter,\n        // i.e. no cards are visible since none exist\n        if (firstVisibleIndex < 0 || lastVisibleIndex < 0) {\n            brazelog {\n                \"Not marking all on-screen cards as read. \" +\n                    \"Either the first or last index is negative. First visible: \" +\n                    \"$firstVisibleIndex . Last visible: $lastVisibleIndex\"\n            }\n            return\n        }\n\n        // We want to mark all cards in the inclusive range of [first, last] as read\n        for (i in firstVisibleIndex..lastVisibleIndex) {\n            val card = getCardAtIndex(i)\n            if (card != null) {\n                card.isIndicatorHighlighted = true\n            }\n        }\n        handler.post {\n            // We add 1 since the number of items since if indices 0 & 1\n            // were changed, then a total of 2 items were changed.\n            val itemsChangedCount = lastVisibleIndex - firstVisibleIndex + 1\n            notifyItemRangeChanged(firstVisibleIndex, itemsChangedCount)\n        }\n    }\n\n    /**\n     * Returns whether the card at the adapter position is a control card.\n     *\n     * @see Card.isControl\n     * @param adapterPosition A valid adapter position for the card data. Invalid positions will return false.\n     */\n    fun isControlCardAtPosition(adapterPosition: Int): Boolean {\n        val card = getCardAtIndex(adapterPosition)\n        return card != null && card.isControl\n    }\n\n    @VisibleForTesting\n    fun getCardAtIndex(index: Int): Card? {\n        if (isInvalidIndex(index)) {\n            brazelog { \"Cannot return card at index: $index in cards list of size: ${cardData.size}\" }\n            return null\n        }\n        return cardData[index]\n    }\n\n    /**\n     * Gets whether the item at a position is visible on screen.\n     */\n    @VisibleForTesting\n    fun isAdapterPositionOnScreen(adapterPosition: Int): Boolean {\n        // At various points in the layout/scroll phase of the RecyclerView, the values\n        // returned by \"find*VisibleItem\" and \"find*CompletelyVisibleItem\" will either\n        // be RecyclerView.NO_POSITION (which is -1), differ by 1, or be equal.\n        // Additionally, the \"find*CompletelyVisible\" value will sometimes update before\n        // the \"find*Visible\" value. To accommodate each of these cases, we'll just take\n        // the min of the \"first\" values and the max of the \"last\" values.\n        val firstItemPosition = min(layoutManager.findFirstVisibleItemPosition(), layoutManager.findFirstCompletelyVisibleItemPosition())\n        val lastItemPosition = max(layoutManager.findLastVisibleItemPosition(), layoutManager.findLastCompletelyVisibleItemPosition())\n        return adapterPosition in firstItemPosition..lastItemPosition\n    }\n\n    /**\n     * Logs an impression on the card. Performs a check against the known set previously impressed card ids.\n     * Will also set the viewed state of the card to true.\n     */\n    @VisibleForTesting\n    fun logImpression(card: Card?) {\n        if (card == null) {\n            return\n        }\n        if (!impressedCardIdsInternal.contains(card.id)) {\n            card.logImpression()\n            impressedCardIdsInternal.add(card.id)\n            brazelog(V) { \"Logged impression for card ${card.id}\" }\n        } else {\n            brazelog(V) { \"Already counted impression for card ${card.id}\" }\n        }\n        if (!card.viewed) {\n            card.viewed = true\n        }\n    }\n\n    private fun isInvalidIndex(index: Int) =\n        index < 0 || index >= cardData.size\n\n    /**\n     * A [Card] based implementation of the [DiffUtil.Callback]. This implementation assumes cards with the same id\n     * are equivalent content-wise and visually.\n     */\n    private class CardListDiffCallback(private val oldCards: List<Card>, private val newCards: List<Card>) : DiffUtil.Callback() {\n        override fun getOldListSize() =\n            oldCards.size\n\n        override fun getNewListSize() =\n            newCards.size\n\n        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =\n            doItemsShareIds(oldItemPosition, newItemPosition)\n\n        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =\n            doItemsShareIds(oldItemPosition, newItemPosition)\n\n        private fun doItemsShareIds(oldItemPosition: Int, newItemPosition: Int) =\n            oldCards[oldItemPosition].id == newCards[newItemPosition].id\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/adapters/EmptyContentCardsAdapter.kt",
    "content": "package com.braze.ui.contentcards.adapters\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport com.braze.ui.R\n\n/**\n * This adapter displays a single, full width/height item. This item\n * should denote that the Content Cards contains no items and is empty.\n */\nopen class EmptyContentCardsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {\n    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_content_cards_empty, viewGroup, false)\n        return NetworkUnavailableViewHolder(view)\n    }\n\n    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {\n        // This method is here since all adapters require a bind implementation. This adapter is static, so no binding occurs.\n    }\n\n    override fun getItemCount() = 1\n\n    internal class NetworkUnavailableViewHolder(view: View) : RecyclerView.ViewHolder(view)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/handlers/DefaultContentCardsUpdateHandler.kt",
    "content": "package com.braze.ui.contentcards.handlers\n\nimport android.os.Parcel\nimport android.os.Parcelable\nimport com.braze.events.ContentCardsUpdatedEvent\nimport com.braze.models.cards.Card\nimport com.braze.ui.actions.brazeactions.containsInvalidBrazeAction\n\nclass DefaultContentCardsUpdateHandler : IContentCardsUpdateHandler {\n    override fun handleCardUpdate(event: ContentCardsUpdatedEvent): List<Card> {\n        // Sort by pinned, then by the 'updated' timestamp descending\n        // Pinned before non-pinned\n        val cardComparator = Comparator { cardA: Card, cardB: Card ->\n            when {\n                // A displays above B since A is pinned and B isn't\n                cardA.isPinned && !cardB.isPinned -> -1\n                // B displays above A since B is pinned and A isn't\n                !cardA.isPinned && cardB.isPinned -> 1\n                // At this point, both A & B are pinned or both A & B are non-pinned\n                // A displays above B if A is newer\n                cardA.created > cardB.created -> -1\n                // B displays above A if B is newer\n                cardA.created < cardB.created -> 1\n                // They're considered equal at this point\n                else -> 0\n            }\n        }\n        return event.allCards.filter { card -> !card.containsInvalidBrazeAction() }\n            .sortedWith(cardComparator)\n    }\n\n    // Parcelable interface method\n    override fun describeContents() = 0\n\n    // Parcelable interface method\n    override fun writeToParcel(dest: Parcel, flags: Int) {\n        // No state is kept in this class so the parcel is left unmodified\n    }\n\n    companion object {\n        // Interface that must be implemented and provided as a public CREATOR\n        // field that generates instances of your Parcelable class from a Parcel.\n        @JvmField\n        val CREATOR: Parcelable.Creator<DefaultContentCardsUpdateHandler> = object : Parcelable.Creator<DefaultContentCardsUpdateHandler> {\n            override fun createFromParcel(source: Parcel) =\n                DefaultContentCardsUpdateHandler()\n\n            override fun newArray(size: Int): Array<DefaultContentCardsUpdateHandler?> =\n                arrayOfNulls(size)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/handlers/DefaultContentCardsViewBindingHandler.kt",
    "content": "package com.braze.ui.contentcards.handlers\n\nimport android.content.Context\nimport android.os.Parcel\nimport android.os.Parcelable\nimport android.view.ViewGroup\nimport androidx.annotation.VisibleForTesting\nimport com.braze.enums.CardType\nimport com.braze.enums.CardType.Companion.fromValue\nimport com.braze.models.cards.Card\nimport com.braze.ui.contentcards.view.BannerImageContentCardView\nimport com.braze.ui.contentcards.view.BaseContentCardView\nimport com.braze.ui.contentcards.view.CaptionedImageContentCardView\nimport com.braze.ui.contentcards.view.ContentCardViewHolder\nimport com.braze.ui.contentcards.view.DefaultContentCardView\nimport com.braze.ui.contentcards.view.ShortNewsContentCardView\nimport com.braze.ui.contentcards.view.TextAnnouncementContentCardView\nimport java.util.*\n\nclass DefaultContentCardsViewBindingHandler : IContentCardsViewBindingHandler {\n    /**\n     * A cache for the views used in binding the items in the [RecyclerView].\n     */\n    private val contentCardViewCache: MutableMap<CardType, BaseContentCardView<*>> = mutableMapOf()\n\n    override fun onCreateViewHolder(\n        context: Context,\n        cards: List<Card>,\n        viewGroup: ViewGroup,\n        viewType: Int\n    ): ContentCardViewHolder {\n        val cardType = fromValue(viewType)\n        return getContentCardsViewFromCache(context, cardType).createViewHolder(viewGroup)\n    }\n\n    override fun onBindViewHolder(\n        context: Context,\n        cards: List<Card>,\n        viewHolder: ContentCardViewHolder,\n        adapterPosition: Int\n    ) {\n        if (adapterPosition < 0 || adapterPosition >= cards.size) {\n            return\n        }\n        val cardAtPosition = cards[adapterPosition]\n        val contentCardView = getContentCardsViewFromCache(\n            context,\n            cardAtPosition.cardType\n        )\n        contentCardView.bindViewHolder(viewHolder, cardAtPosition)\n    }\n\n    override fun getItemViewType(\n        context: Context,\n        cards: List<Card>,\n        adapterPosition: Int\n    ): Int {\n        if (adapterPosition < 0 || adapterPosition >= cards.size) {\n            return -1\n        }\n        val card = cards[adapterPosition]\n        return card.cardType.value\n    }\n\n    /**\n     * Gets a cached instance of a [BaseContentCardView] for view creation/binding for a given [CardType].\n     * If the [CardType] is not found in the cache, then a view binding implementation for that [CardType]\n     * is created and added to the cache.\n     */\n    @VisibleForTesting\n    fun getContentCardsViewFromCache(context: Context, cardType: CardType): BaseContentCardView<*> {\n        if (!contentCardViewCache.containsKey(cardType) || contentCardViewCache[cardType] == null) {\n            // Create the view here\n            val contentCardView: BaseContentCardView<*> = when (cardType) {\n                CardType.BANNER -> BannerImageContentCardView(context)\n                CardType.CAPTIONED_IMAGE -> CaptionedImageContentCardView(context)\n                CardType.SHORT_NEWS -> ShortNewsContentCardView(context)\n                CardType.TEXT_ANNOUNCEMENT -> TextAnnouncementContentCardView(context)\n                else -> DefaultContentCardView(context)\n            }\n            contentCardViewCache[cardType] = contentCardView\n        }\n        return contentCardViewCache[cardType] ?: DefaultContentCardView(context)\n    }\n\n    // Parcelable interface method\n    override fun describeContents(): Int = 0\n\n    // Parcelable interface method\n    override fun writeToParcel(dest: Parcel, flags: Int) {\n        // Retaining views across a transition could lead to a\n        // resource leak so the parcel is left unmodified\n    }\n\n    companion object {\n        // Interface that must be implemented and provided as a public CREATOR\n        // field that generates instances of your Parcelable class from a Parcel.\n        @JvmField\n        val CREATOR: Parcelable.Creator<DefaultContentCardsViewBindingHandler> =\n            object : Parcelable.Creator<DefaultContentCardsViewBindingHandler> {\n                override fun createFromParcel(source: Parcel) =\n                    DefaultContentCardsViewBindingHandler()\n\n                override fun newArray(size: Int): Array<DefaultContentCardsViewBindingHandler?> =\n                    arrayOfNulls(size)\n            }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/handlers/IContentCardsUpdateHandler.kt",
    "content": "package com.braze.ui.contentcards.handlers\n\nimport android.os.Parcelable\nimport com.braze.models.cards.Card\nimport com.braze.events.ContentCardsUpdatedEvent\n\n/**\n * An interface to handle card updates for the [Card]. Handles the\n * sorting for [ContentCardsUpdatedEvent]'s in the [ContentCardsFragment].\n */\ninterface IContentCardsUpdateHandler : Parcelable {\n    /**\n     * Handles a [ContentCardsUpdatedEvent] and returns a list of [Card] for rendering.\n     * Each [ContentCardsUpdatedEvent] will contain\n     * the full list of cards from either the cache or from a network request.\n     *\n     * @param event the [ContentCardsUpdatedEvent] update.\n     * @return a list of [Card] to be rendered in the Content Cards from this [ContentCardsUpdatedEvent]\n     */\n    fun handleCardUpdate(event: ContentCardsUpdatedEvent): List<Card>\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/handlers/IContentCardsViewBindingHandler.kt",
    "content": "package com.braze.ui.contentcards.handlers\n\nimport android.content.Context\nimport android.os.Parcelable\nimport android.view.ViewGroup\nimport com.braze.models.cards.Card\nimport com.braze.ui.contentcards.view.ContentCardViewHolder\n\n/**\n * An interface to define how the cards display in the [ContentCardsFragment]. The methods here\n * closely mirror those of [RecyclerView.Adapter] and are called as part of those methods in\n * the [ContentCardAdapter].\n */\ninterface IContentCardsViewBindingHandler : Parcelable {\n    /**\n     * Creates an [ContentCardViewHolder] of the given type to represent an item in the ContentCards. You can create\n     * a new View manually or inflate it from an XML layout file.\n     *\n     * The new [ContentCardViewHolder] will be used to display adapter items\n     * using [IContentCardsViewBindingHandler.onBindViewHolder].\n     *\n     * @see RecyclerView.Adapter.onCreateViewHolder\n     * @param context The application context\n     * @param cards The collection of card items in the adapter. Should not be modified.\n     * @param viewGroup The [ViewGroup] into which the new View will be added after it is bound to an adapter position.\n     * @param viewType The view type of the new View.\n     * @return A new [ContentCardViewHolder] that holds a View of the given view type.\n     */\n    fun onCreateViewHolder(\n        context: Context,\n        cards: List<Card>,\n        viewGroup: ViewGroup,\n        viewType: Int\n    ): ContentCardViewHolder\n\n    /**\n     * Called to display the data at the specified adapter position. This method should update the contents of the\n     * [ContentCardViewHolder.itemView] to reflect the item at the given adapter position.\n     *\n     * @see RecyclerView.Adapter.onBindViewHolder\n     * @param context The application context.\n     * @param cards The collection of card items in the adapter. Should not be modified.\n     * @param viewHolder The [ContentCardViewHolder] which should be updated to represent the contents\n     * of the item at the given adapter position.\n     * @param adapterPosition The position of the item within the adapter's card items.\n     */\n    fun onBindViewHolder(\n        context: Context,\n        cards: List<Card>,\n        viewHolder: ContentCardViewHolder,\n        adapterPosition: Int\n    )\n\n    /**\n     * Returns the view type of the item at the given position for the purposes of view recycling purposes.\n     *\n     * @param context The application context.\n     * @param cards The collection of card items in the adapter. Should not be modified.\n     * @param adapterPosition The position of the item within the adapter's card items.\n     * @return A value identifying the type of the view needed to represent the item at position. Type values need not be contiguous.\n     */\n    fun getItemViewType(\n        context: Context,\n        cards: List<Card>,\n        adapterPosition: Int\n    ): Int\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/listeners/DefaultContentCardsActionListener.kt",
    "content": "package com.braze.ui.contentcards.listeners\n\n// Use the default implementations from the interface\nclass DefaultContentCardsActionListener : IContentCardsActionListener\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/listeners/IContentCardsActionListener.kt",
    "content": "package com.braze.ui.contentcards.listeners\n\nimport android.content.Context\nimport com.braze.models.cards.Card\nimport com.braze.ui.actions.IAction\n\n/**\n * The [IContentCardsActionListener] receives the [Card] when a user action such as\n * clicking or dismissal is performed and gives the host app the ability to override\n * Braze's default procedure for the user action.\n */\ninterface IContentCardsActionListener {\n    /**\n     * @param context The context.\n     * @param card The card that has been clicked.\n     * @param cardAction The action associated with the card being clicked\n     *\n     * @return boolean Flag to indicate to Braze whether the click should be handled by\n     * the host app. If true, Braze will log a card click and do nothing else. If false,\n     * Braze will continue its typical handling of the click.\n     */\n    fun onContentCardClicked(context: Context, card: Card, cardAction: IAction?): Boolean = false\n\n    /**\n     * Note that the [Card] will be off-screen by the time this function is called.\n     *\n     * @param context The context.\n     * @param card The card that has been dismissed.\n     */\n    fun onContentCardDismissed(context: Context, card: Card) {\n        // The default implementation of this is \"do nothing\"\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/managers/BrazeContentCardsManager.kt",
    "content": "package com.braze.ui.contentcards.managers\n\nimport com.braze.ui.contentcards.listeners.DefaultContentCardsActionListener\nimport com.braze.ui.contentcards.listeners.IContentCardsActionListener\n\nopen class BrazeContentCardsManager {\n    /**\n     * The current implementation of the [IContentCardsActionListener] interface.\n     * Defaults to [DefaultContentCardsActionListener].\n     * Will also be set to the default when set to null.\n     */\n    var contentCardsActionListener: IContentCardsActionListener? = DefaultContentCardsActionListener()\n        set(value) {\n            field = value ?: DefaultContentCardsActionListener()\n        }\n\n    companion object {\n        @JvmStatic\n        val instance by lazy {\n            BrazeContentCardsManager()\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/recycler/ContentCardsDividerItemDecoration.kt",
    "content": "package com.braze.ui.contentcards.recycler\n\nimport android.content.Context\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport com.braze.ui.R\nimport com.braze.ui.contentcards.adapters.ContentCardAdapter\n\n/**\n * Manages the divider logic of cards in the ContentCards. To center align cards, this sets a left padding on each view\n * based on the configured maximum width of the Content Cards and the size of the screen.\n * To add a divider between cards, sets a top padding on each card.\n *\n * Reads the item divider values from the \"dimens\" resource.\n * For the item divider height: \"R.dimen.com_braze_content_cards_divider_height\"\n * For the item max width: \"R.dimen.com_braze_content_cards_max_width\"\n */\nopen class ContentCardsDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {\n    private val appContext: Context = context.applicationContext\n\n    /**\n     * The Content Cards dividerHeight from styles in pixels after conversion from dp.\n     */\n    private val itemDividerHeight = appContext.resources.getDimensionPixelSize(R.dimen.com_braze_content_cards_divider_height)\n\n    /**\n     * The width of the Content Cards from the dimens value in pixels after conversion from dp.\n     */\n    private val contentCardsItemMaxWidth = appContext.resources.getDimensionPixelSize(R.dimen.com_braze_content_cards_max_width)\n\n    override fun getItemOffsets(\n        itemViewOutputRect: Rect,\n        view: View,\n        parent: RecyclerView,\n        state: RecyclerView.State\n    ) {\n        super.getItemOffsets(itemViewOutputRect, view, parent, state)\n        val childAdapterPosition = parent.getChildAdapterPosition(view)\n        var isControlCard = false\n        if (parent.adapter is ContentCardAdapter) {\n            val cardAdapter = parent.adapter as ContentCardAdapter\n            isControlCard = cardAdapter.isControlCardAtPosition(childAdapterPosition)\n        }\n\n        // The goal here is to ensure that:\n        // * The first visible card in the has a margin to the top\n        // * All cards have the same margin between them\n        // * The last visible card has a margin to the bottom\n\n        // Only the first card in the list gets a top divider. All other non-control cards get a bottom divider.\n        itemViewOutputRect.top = if (childAdapterPosition == 0) itemDividerHeight else 0\n        itemViewOutputRect.bottom = if (isControlCard) 0 else itemDividerHeight\n\n        // Now we have to center the view horizontally in the RecyclerView\n        // by adding in a margin on to the left & right of the view\n        val sidePadding = getSidePaddingValue(parent.width)\n        itemViewOutputRect.left = sidePadding\n        itemViewOutputRect.right = sidePadding\n    }\n\n    /**\n     * Calculates the padding value in screen pixels using the width of the parent view and the predefined item\n     * view max width.\n     */\n    private fun getSidePaddingValue(parentWidth: Int): Int {\n        val padding = (parentWidth - contentCardsItemMaxWidth) / 2\n        return padding.coerceAtLeast(0)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/recycler/ItemTouchHelperAdapter.kt",
    "content": "package com.braze.ui.contentcards.recycler\n\ninterface ItemTouchHelperAdapter {\n    fun onItemDismiss(position: Int)\n\n    /**\n     * @param position The adapter position of the item.\n     * @return True if the item at the adapter position is dismissable, false if the item is not dismissable.\n     */\n    fun isItemDismissable(position: Int): Boolean\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/recycler/SimpleItemTouchHelperCallback.kt",
    "content": "package com.braze.ui.contentcards.recycler\n\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\n\nopen class SimpleItemTouchHelperCallback(private val adapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {\n    override fun isLongPressDragEnabled() =\n        false\n\n    override fun isItemViewSwipeEnabled() =\n        true\n\n    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {\n        // Since dragging is disabled, mask the drag flag to 0\n        val dragFlags = 0\n        // Only let the item be swiped if the item is dismissable\n        val swipeFlags = if (adapter.isItemDismissable(viewHolder.bindingAdapterPosition)) ItemTouchHelper.START else 0\n        return makeMovementFlags(dragFlags, swipeFlags)\n    }\n\n    override fun onMove(\n        recyclerView: RecyclerView,\n        viewHolder: RecyclerView.ViewHolder,\n        target: RecyclerView.ViewHolder\n    ) =\n        // Since we don't support drag & drop, this method will never get called. Thus this return value is never used.\n        false\n\n    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) =\n        adapter.onItemDismiss(viewHolder.bindingAdapterPosition)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/BannerImageContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport com.braze.models.cards.BannerImageCard\nimport com.braze.models.cards.Card\nimport com.braze.ui.R\n\nopen class BannerImageContentCardView(context: Context) : BaseContentCardView<BannerImageCard>(\n    context\n) {\n    private inner class ViewHolder(view: View) :\n        ContentCardViewHolder(view, isUnreadIndicatorEnabled) {\n        val imageView: ImageView? = view.findViewById(R.id.com_braze_content_cards_banner_image_card_image)\n    }\n\n    override fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_banner_image_content_card, viewGroup, false)\n        setViewBackground(view)\n        return ViewHolder(view)\n    }\n\n    override fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        if (card is BannerImageCard) {\n            super.bindViewHolder(viewHolder, card)\n            val bannerImageViewHolder = viewHolder as ViewHolder\n            setOptionalCardImage(\n                bannerImageViewHolder.imageView,\n                card.aspectRatio,\n                card.imageUrl,\n                card\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/BaseContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.annotation.TargetApi\nimport android.content.Context\nimport android.os.Build\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport com.braze.models.cards.Card\nimport com.braze.ui.R\nimport com.braze.ui.widget.BaseCardView\nimport com.braze.ui.actions.IAction\nimport com.braze.ui.contentcards.managers.BrazeContentCardsManager.Companion.instance\n\n/**\n * Base class for ContentCard views.\n */\nabstract class BaseContentCardView<T : Card>(context: Context) : BaseCardView<T>(\n    context\n) {\n    abstract fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder\n\n    open fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        viewHolder.setPinnedIconVisible(card.isPinned)\n        viewHolder.setUnreadBarVisible(\n            configurationProvider.isContentCardsUnreadVisualIndicatorEnabled\n                && !card.isIndicatorHighlighted\n        )\n        val cardAction = getUriActionForCard(card)\n        viewHolder.itemView.setOnClickListener {\n            handleCardClick(\n                applicationContext,\n                card,\n                cardAction\n            )\n        }\n\n        // Only set the action hint to visible if there's a card action\n        viewHolder.setActionHintVisible(cardAction != null)\n    }\n\n    /**\n     * Sets the card's image to a given url. The view may be null.\n     *\n     * @param imageView          The ImageView\n     * @param cardAspectRatio    The aspect ratio as set by the card itself\n     * @param cardImageUrl       The image url\n     * @param card               The card being rendered\n     */\n    fun setOptionalCardImage(\n        imageView: ImageView?,\n        cardAspectRatio: Float,\n        cardImageUrl: String?,\n        card: Card\n    ) {\n        if (imageView != null && cardImageUrl != null) {\n            setImageViewToUrl(imageView, cardImageUrl, cardAspectRatio, card)\n        }\n    }\n\n    override fun isClickHandled(context: Context, card: Card, cardAction: IAction?): Boolean =\n        instance.contentCardsActionListener?.onContentCardClicked(context, card, cardAction) == true\n\n    @Suppress(\"MagicNumber\")\n    @TargetApi(21)\n    protected fun safeSetClipToOutline(imageView: ImageView?) {\n        imageView?.clipToOutline = true\n    }\n\n    // getDrawable() is deprecated but the alternative is supported by 21+\n    protected fun setViewBackground(view: View) {\n        @Suppress(\"deprecation\")\n        view.background = resources.getDrawable(R.drawable.com_braze_content_card_background)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            @Suppress(\"deprecation\")\n            view.foreground = resources.getDrawable(R.drawable.com_braze_content_card_scrim)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/CaptionedImageContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport com.braze.models.cards.CaptionedImageCard\nimport com.braze.models.cards.Card\nimport com.braze.ui.R\n\nopen class CaptionedImageContentCardView(context: Context) : BaseContentCardView<CaptionedImageCard>(\n    context\n) {\n    private inner class ViewHolder constructor(view: View) :\n        ContentCardViewHolder(view, isUnreadIndicatorEnabled) {\n        val title: TextView? = view.findViewById(R.id.com_braze_content_cards_captioned_image_title)\n        val description: TextView? = view.findViewById(R.id.com_braze_content_cards_captioned_image_description)\n        val imageView: ImageView? = view.findViewById(R.id.com_braze_content_cards_captioned_image_card_image)\n    }\n\n    override fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_captioned_image_content_card, viewGroup, false)\n        setViewBackground(view)\n        return ViewHolder(view)\n    }\n\n    override fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        if (card is CaptionedImageCard) {\n            super.bindViewHolder(viewHolder, card)\n            val captionedImageViewHolder = viewHolder as ViewHolder\n            captionedImageViewHolder.title?.let { setOptionalTextView(it, card.title) }\n            captionedImageViewHolder.description?.let { setOptionalTextView(it, card.description) }\n            (if (card.domain.isNullOrBlank()) card.url else card.domain)?.let {\n                captionedImageViewHolder.setActionHintText(\n                    it\n                )\n            }\n            setOptionalCardImage(\n                captionedImageViewHolder.imageView,\n                card.aspectRatio,\n                card.imageUrl,\n                card\n            )\n            viewHolder.itemView.contentDescription = \"${card.title} .  ${card.description}\"\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/ContentCardViewHolder.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport com.braze.ui.R\n\nopen class ContentCardViewHolder(view: View, showUnreadIndicator: Boolean) :\n    RecyclerView.ViewHolder(view) {\n    private val unreadBar: View? = view.findViewById(R.id.com_braze_content_cards_unread_bar)\n    private val pinnedIcon: ImageView? = view.findViewById(R.id.com_braze_content_cards_pinned_icon)\n    private val actionHint: TextView? = view.findViewById(R.id.com_braze_content_cards_action_hint)\n\n    init {\n        if (showUnreadIndicator) {\n            unreadBar?.visibility = View.VISIBLE\n            // Round the edges at the bottom\n            // getDrawable() is deprecated but the alternative is supported by 21+\n            @Suppress(\"deprecation\")\n            unreadBar?.background =\n                view.context.resources.getDrawable(R.drawable.com_braze_content_cards_unread_bar_background)\n        } else {\n            unreadBar?.visibility = View.GONE\n        }\n    }\n\n    /**\n     * Sets the pinned icon to [View.VISIBLE] when true, or [View.GONE] otherwise.\n     *\n     * @param isVisible Should the pinned icon be visible on the card.\n     */\n    fun setPinnedIconVisible(isVisible: Boolean) {\n        pinnedIcon?.visibility = if (isVisible) View.VISIBLE else View.GONE\n    }\n\n    /**\n     * Sets the unread bar to [View.VISIBLE] when true, or [View.GONE] otherwise.\n     *\n     * @param isVisible Should the unread bar be visible on the card.\n     */\n    fun setUnreadBarVisible(isVisible: Boolean) {\n        unreadBar?.visibility = if (isVisible) View.VISIBLE else View.GONE\n    }\n\n    /**\n     * Sets the action hint to [View.VISIBLE] when true, or [View.GONE] otherwise.\n     *\n     * @param isVisible Should the action hint be visible on the card.\n     */\n    fun setActionHintVisible(isVisible: Boolean) {\n        actionHint?.visibility = if (isVisible) View.VISIBLE else View.GONE\n    }\n\n    /**\n     * Sets the action hint text.\n     */\n    fun setActionHintText(text: String) {\n        actionHint?.text = text\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/DefaultContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport com.braze.models.cards.Card\nimport com.braze.ui.R\n\n/**\n * A view for when the card type is unknown or otherwise can't be rendered.\n */\nopen class DefaultContentCardView(context: Context) : BaseContentCardView<Card>(\n    context\n) {\n    override fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_default_content_card, viewGroup, false)\n        return ContentCardViewHolder(view, false)\n    }\n\n    override fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        // Do nothing here since default cards are not meant to be displayed.\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/ShortNewsContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.content.Context\nimport android.os.Build\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport com.braze.models.cards.Card\nimport com.braze.models.cards.ShortNewsCard\nimport com.braze.ui.R\n\nopen class ShortNewsContentCardView(context: Context) : BaseContentCardView<ShortNewsCard>(\n    context\n) {\n    private inner class ViewHolder constructor(view: View) :\n        ContentCardViewHolder(view, isUnreadIndicatorEnabled) {\n        val title: TextView? = view.findViewById(R.id.com_braze_content_cards_short_news_card_title)\n        val description: TextView? = view.findViewById(R.id.com_braze_content_cards_short_news_card_description)\n        val imageView: ImageView? = view.findViewById(R.id.com_braze_content_cards_short_news_card_image)\n    }\n\n    override fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_short_news_content_card, viewGroup, false)\n        setViewBackground(view)\n        return ViewHolder(view)\n    }\n\n    override fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        if (card is ShortNewsCard) {\n            super.bindViewHolder(viewHolder, card)\n            val shortNewsCardViewHolder = viewHolder as ViewHolder\n            shortNewsCardViewHolder.title?.let { setOptionalTextView(it, card.title) }\n            shortNewsCardViewHolder.description?.let { setOptionalTextView(it, card.description) }\n            val actionHintText = if (card.domain.isNullOrBlank()) card.url else card.domain\n            actionHintText?.let { shortNewsCardViewHolder.setActionHintText(it) }\n\n            setOptionalCardImage(\n                shortNewsCardViewHolder.imageView,\n                ASPECT_RATIO,\n                card.imageUrl,\n                card\n            )\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                safeSetClipToOutline(shortNewsCardViewHolder.imageView)\n            }\n            viewHolder.itemView.contentDescription = \"${card.title} . ${card.description}\"\n        }\n    }\n\n    companion object {\n        // This value will be the aspect ratio of the card on render.\n        private const val ASPECT_RATIO = 1f\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/contentcards/view/TextAnnouncementContentCardView.kt",
    "content": "package com.braze.ui.contentcards.view\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport com.braze.models.cards.Card\nimport com.braze.models.cards.TextAnnouncementCard\nimport com.braze.ui.R\n\nopen class TextAnnouncementContentCardView(context: Context) :\n    BaseContentCardView<TextAnnouncementCard>(\n        context\n    ) {\n    private inner class ViewHolder constructor(view: View) :\n        ContentCardViewHolder(view, isUnreadIndicatorEnabled) {\n        val title: TextView? = view.findViewById(R.id.com_braze_content_cards_text_announcement_card_title)\n        val description: TextView? = view.findViewById(R.id.com_braze_content_cards_text_announcement_card_description)\n    }\n\n    override fun createViewHolder(viewGroup: ViewGroup): ContentCardViewHolder {\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.com_braze_text_announcement_content_card, viewGroup, false)\n        setViewBackground(view)\n        return ViewHolder(view)\n    }\n\n    override fun bindViewHolder(viewHolder: ContentCardViewHolder, card: Card) {\n        if (card is TextAnnouncementCard) {\n            super.bindViewHolder(viewHolder, card)\n            val textAnnouncementViewHolder = viewHolder as ViewHolder\n            textAnnouncementViewHolder.title?.let { setOptionalTextView(it, card.title) }\n            textAnnouncementViewHolder.description?.let {\n                setOptionalTextView(\n                    it,\n                    card.description\n                )\n            }\n            val actionHintText = if (card.domain.isNullOrBlank()) card.url else card.domain\n            actionHintText?.let { textAnnouncementViewHolder.setActionHintText(it) }\n            viewHolder.itemView.contentDescription = \"${card.title} . ${card.description}\"\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/feed/BrazeFeedManager.java",
    "content": "package com.braze.ui.feed;\n\nimport com.braze.ui.feed.listeners.BrazeDefaultFeedClickActionListener;\nimport com.braze.ui.feed.listeners.IFeedClickActionListener;\n\npublic class BrazeFeedManager {\n  private static volatile BrazeFeedManager sInstance = null;\n\n  // card click listeners\n  private IFeedClickActionListener mCustomFeedClickActionListener;\n  private final IFeedClickActionListener mDefaultFeedClickActionListener = new BrazeDefaultFeedClickActionListener();\n\n  public static BrazeFeedManager getInstance() {\n    if (sInstance == null) {\n      synchronized (BrazeFeedManager.class) {\n        if (sInstance == null) {\n          sInstance = new BrazeFeedManager();\n        }\n      }\n    }\n    return sInstance;\n  }\n\n  /**\n   * Assigns a custom {@link IFeedClickActionListener} that will be used to handle news feed card\n   * click actions.\n   *\n   * @param customNewsFeedClickActionListener A custom implementation of\n   * {@link IFeedClickActionListener}\n   */\n  public void setFeedCardClickActionListener(IFeedClickActionListener customNewsFeedClickActionListener) {\n    mCustomFeedClickActionListener = customNewsFeedClickActionListener;\n  }\n\n  /**\n   * @return the assigned implementation of the {@link IFeedClickActionListener} interface.\n   */\n  public IFeedClickActionListener getFeedCardClickActionListener() {\n    return mCustomFeedClickActionListener != null ? mCustomFeedClickActionListener : mDefaultFeedClickActionListener;\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/feed/BrazeImageSwitcher.kt",
    "content": "package com.braze.ui.feed\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.widget.ImageSwitcher\nimport androidx.annotation.VisibleForTesting\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.R\n\nclass BrazeImageSwitcher(context: Context, attrs: AttributeSet?) : ImageSwitcher(context, attrs) {\n    @set:VisibleForTesting\n    var readIcon: Drawable? = null\n\n    @set:VisibleForTesting\n    var unReadIcon: Drawable? = null\n\n    init {\n        try {\n            // Get the array of offset indices into the R value array defined for this view.\n            // The R value array is at R.styleable.com_braze_ui_feed_BrazeImageSwitcher.\n            val typedArray =\n                context.obtainStyledAttributes(attrs, R.styleable.com_braze_ui_feed_BrazeImageSwitcher)\n\n            // For all offsets defined on this view, if the offset is equal to the offset for the custom font file\n            // defined at R.styleable.com_braze_ui_feed_BrazeImageSwitcher_brazeFeedCustomReadIcon or\n            // R.styleable.com_braze_ui_feed_BrazeImageSwitcher_brazeFeedCustomUnReadIcon,\n            // instruct the typed array to retrieve the data at that offset.\n            for (i in 0 until typedArray.indexCount) {\n                val offset = typedArray.getIndex(i)\n                if (offset == R.styleable.com_braze_ui_feed_BrazeImageSwitcher_brazeFeedCustomReadIcon) {\n                    val drawable = typedArray.getDrawable(offset)\n                    if (drawable != null) {\n                        readIcon = drawable\n                    }\n                } else if (typedArray.getIndex(i) == R.styleable.com_braze_ui_feed_BrazeImageSwitcher_brazeFeedCustomUnReadIcon) {\n                    val drawable = typedArray.getDrawable(offset)\n                    if (drawable != null) {\n                        unReadIcon = drawable\n                    }\n                }\n            }\n            typedArray.recycle()\n        } catch (e: Exception) {\n            brazelog(W, e) { \"Error while checking for custom drawable.\" }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/feed/listeners/BrazeDefaultFeedClickActionListener.java",
    "content": "package com.braze.ui.feed.listeners;\n\nimport android.content.Context;\n\nimport com.braze.models.cards.Card;\nimport com.braze.ui.actions.IAction;\n\npublic class BrazeDefaultFeedClickActionListener implements IFeedClickActionListener {\n  @Override\n  public boolean onFeedCardClicked(Context context, Card card, IAction cardAction) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/feed/listeners/IFeedClickActionListener.java",
    "content": "package com.braze.ui.feed.listeners;\n\nimport android.content.Context;\n\nimport com.braze.models.cards.Card;\nimport com.braze.ui.actions.IAction;\n\n/**\n * The IFeedClickActionListener receives the news feed card when a\n * news feed click action is performed and gives the host app the ability to\n * override Braze's default procedure when handling news feed card clicks.\n *\n * See {@link com.braze.ui.feed.BrazeFeedManager} and {@link com.braze.ui.widget.BaseCardView}\n */\npublic interface IFeedClickActionListener {\n\n  /**\n   * @param context the context of the news feed.\n   * @param card the news feed card that has been clicked.\n   * @param cardAction the action associated with the card being clicked\n   *\n   * @return boolean flag to indicate to Braze whether the click is being handled by\n   * the host app. If true Braze will log a card click and do nothing. If false\n   * Braze will continue its typical handling of the news feed card click.\n   */\n  boolean onFeedCardClicked(Context context, Card card, IAction cardAction);\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/feed/view/BaseFeedCardView.java",
    "content": "package com.braze.ui.feed.view;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewStub;\nimport android.widget.ImageView;\n\nimport com.braze.models.cards.Card;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.BrazeFeedManager;\nimport com.braze.ui.widget.BaseCardView;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.actions.IAction;\n\n/**\n * Base class for Braze feed card views\n */\npublic abstract class BaseFeedCardView<T extends Card> extends BaseCardView<T> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BaseCardView.class);\n\n  public BaseFeedCardView(Context context) {\n    super(context);\n\n    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n    inflater.inflate(getLayoutResource(), this);\n    // All implementing views of BaseCardView must include this switcher view in order to have the\n    // read/unread functionality. Views that don't have the indicator (like banner views) won't have the image switcher\n    // in them and thus we do the null-check below.\n    imageSwitcher = findViewById(R.id.com_braze_newsfeed_item_read_indicator_image_switcher);\n    if (imageSwitcher != null) {\n      imageSwitcher.setFactory(() -> new ImageView(applicationContext));\n    }\n\n    // If the visual indicator on cards shouldn't be on, due to the xml setting in braze.xml, then set the\n    // imageSwitcher to GONE to hide the indicator UI.\n    // If the setting is false, then hide the indicator.\n    if (!isUnreadIndicatorEnabled()) {\n      if (imageSwitcher != null) {\n        imageSwitcher.setVisibility(GONE);\n      }\n    }\n  }\n\n  /**\n   * Gets the view to display the correct card image. Note that since the Content Cards does not\n   * use image view stubs any longer, this method is only used for Feed cards.\n   *\n   * @param stubLayoutId The resource Id of the stub for inflation as returned by findViewById.\n   * @return the view to display the image.\n   */\n  public View getProperViewFromInflatedStub(int stubLayoutId) {\n    ViewStub imageStub = findViewById(stubLayoutId);\n    imageStub.inflate();\n    return findViewById(R.id.com_braze_stubbed_feed_image_view);\n  }\n\n  /**\n   * This method is called when the setRead() method is called on the internal Card object.\n   */\n\n  public void setCard(final T newCard) {\n    card = newCard;\n    onSetCard(card);\n    card.setListener(() -> setCardViewedIndicator(imageSwitcher, getCard()));\n    setCardViewedIndicator(imageSwitcher, getCard());\n  }\n\n  public Card getCard() {\n    return card;\n  }\n\n  @Override\n  protected boolean isClickHandled(Context context, Card card, IAction cardAction) {\n    return BrazeFeedManager.getInstance().getFeedCardClickActionListener().onFeedCardClicked(context, card, cardAction);\n  }\n\n  protected abstract int getLayoutResource();\n\n  protected abstract void onSetCard(T card);\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/BrazeInAppMessageManager.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.pm.ActivityInfo\nimport androidx.annotation.VisibleForTesting\nimport com.braze.Braze.Companion.getInstance\nimport com.braze.BrazeInternal.retryInAppMessage\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.inappmessage.InAppMessageFailureType\nimport com.braze.enums.inappmessage.Orientation\nimport com.braze.events.IEventSubscriber\nimport com.braze.events.InAppMessageEvent\nimport com.braze.events.SdkDataWipeEvent\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageImmersiveBase\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.getPrettyPrintedString\nimport com.braze.support.wouldPushPermissionPromptDisplay\nimport com.braze.ui.actions.brazeactions.containsAnyPushPermissionBrazeActions\nimport com.braze.ui.actions.brazeactions.containsInvalidBrazeAction\nimport com.braze.ui.inappmessage.listeners.DefaultInAppMessageViewLifecycleListener\nimport com.braze.ui.inappmessage.listeners.IInAppMessageViewLifecycleListener\nimport com.braze.ui.inappmessage.utils.BackgroundInAppMessagePreparer.prepareInAppMessageForDisplay\nimport com.braze.ui.inappmessage.views.IInAppMessageImmersiveView\nimport com.braze.ui.inappmessage.views.IInAppMessageView\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlBaseView\nimport com.braze.ui.support.isCurrentOrientationValid\nimport com.braze.ui.support.isRunningOnTablet\nimport com.braze.ui.support.removeViewFromParent\nimport com.braze.ui.support.setActivityRequestedOrientation\nimport java.util.*\nimport java.util.concurrent.atomic.AtomicBoolean\nimport java.util.concurrent.locks.ReentrantLock\nimport kotlin.concurrent.withLock\n\n/**\n * This class is used to display in-app messages that are either sent from Braze\n * or are created natively in the host app. It will only show one in-app message at a time and will\n * place all other in-app messages onto a stack. The [BrazeInAppMessageManager] will also keep track of in-app\n * impressions and clicks, which can be viewed on the dashboard.\n *\n * If there is already an in-app message being displayed, the new in-app message will be put onto the top of the\n * stack and can be displayed at a later time. If there is no in-app message being displayed, then the\n * [IInAppMessageManagerListener.beforeInAppMessageDisplayed]\n * will be called. The [InAppMessageOperation] return value can be used to\n * control when the in-app message should be displayed. A suggested usage of this method would be to delay\n * in-app message messages in certain parts of the app by returning [InAppMessageOperation.DISPLAY_LATER]\n * when in-app message would be distracting to the users app experience. If the method returns\n * [InAppMessageOperation.DISPLAY_NOW] then the in-app message will be displayed\n * immediately.\n *\n * The [IInAppMessageManagerListener.onInAppMessageClicked]\n * and [IInAppMessageManagerListener.onInAppMessageDismissed]\n * methods can be used to override the default click and dismiss behavior.\n *\n * By default, in-app messages fade in and out from view. The slideup type of in-app message slides in and out of view\n * can be dismissed by swiping the view horizontally. If the in-app message's DismissType is set to AUTO_DISMISS,\n * then the in-app message will animate out of view once the set duration time has elapsed.\n *\n * In order to use a custom view, you must create a custom view factory using the\n * [BrazeInAppMessageManager.setCustomInAppMessageViewFactory] method.\n *\n * A new in-app message [android.view.View] object is created when a in-app message is displayed and also\n * when the user navigates away to another [android.app.Activity]. This happens so that the\n * Activity can be garbage collected and does not create a memory leak. For that reason, the\n * [BrazeInAppMessageManager.registerInAppMessageManager]\n * and [BrazeInAppMessageManager.unregisterInAppMessageManager]\n * must be called in the Activity.onResume() and Activity.onPause()\n * methods of every Activity.\n */\n// Static field leak doesn't apply to this singleton since the activity is nullified after the manager is unregistered.\n@SuppressLint(\"StaticFieldLeak\")\nopen class BrazeInAppMessageManager : InAppMessageManagerBase() {\n    private val inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener =\n        DefaultInAppMessageViewLifecycleListener()\n\n    @JvmField\n    @VisibleForTesting\n    val displayingInAppMessage = AtomicBoolean(false)\n\n    /**\n     * The stack of In-App Messages waiting to be displayed.\n     */\n    @VisibleForTesting\n    val inAppMessageStack = Stack<IInAppMessage>()\n    val inAppMessageEventMap = mutableMapOf<IInAppMessage, InAppMessageEvent>()\n    private var inAppMessageEventSubscriber: IEventSubscriber<InAppMessageEvent>? = null\n    private var sdkDataWipeEventSubscriber: IEventSubscriber<SdkDataWipeEvent>? = null\n    private var originalOrientation: Int? = null\n    private var configurationProvider: BrazeConfigurationProvider? = null\n    private var inAppMessageViewWrapper: IInAppMessageViewWrapper? = null\n\n    /**\n     * An In-App Message being carried over during the\n     * [unregisterInAppMessageManager]\n     * [registerInAppMessageManager] transition.\n     */\n    @VisibleForTesting\n    var carryoverInAppMessage: IInAppMessage? = null\n\n    /**\n     * An In-App Message that could not display after a call to [requestDisplayInAppMessage]\n     * due to no [Activity] being registered via [registerInAppMessageManager].\n     */\n    @VisibleForTesting\n    var unregisteredInAppMessage: IInAppMessage? = null\n\n    /**\n     * Gets whether an in-app message is currently displaying on the device.\n     */\n    val isCurrentlyDisplayingInAppMessage: Boolean\n        get() = displayingInAppMessage.get()\n\n    /**\n     * Ensures the InAppMessageManager is subscribed in-app message events if not already subscribed.\n     * Before this method gets called, the InAppMessageManager is not subscribed to in-app message events\n     * and cannot display them. Every call to registerInAppMessageManager() calls this method.\n     *\n     * If events with triggers are logged before the first call to registerInAppMessageManager(), then the\n     * corresponding in-app message won't display. Thus, if logging events with triggers before the first call\n     * to registerInAppMessageManager(), then call this method to ensure that in-app message events\n     * are correctly handled by the BrazeInAppMessageManager.\n     *\n     * For example, if logging custom events with triggers in your first activity's onCreate(), be sure\n     * to call this method manually beforehand so that the in-app message will get displayed by the time\n     * registerInAppMessageManager() gets called.\n     *\n     * @param context The application context\n     */\n    open fun ensureSubscribedToInAppMessageEvents(context: Context) {\n        if (inAppMessageEventSubscriber != null) {\n            brazelog {\n                \"Removing existing in-app message event subscriber before subscribing a new one.\"\n            }\n            getInstance(context).removeSingleSubscription(\n                inAppMessageEventSubscriber,\n                InAppMessageEvent::class.java\n            )\n        }\n        brazelog { \"Subscribing in-app message event subscriber\" }\n        inAppMessageEventSubscriber = createInAppMessageEventSubscriber().also {\n            getInstance(context).subscribeToNewInAppMessages(it)\n        }\n\n        if (sdkDataWipeEventSubscriber != null) {\n            brazelog(V) { \"Removing existing sdk data wipe event subscriber before subscribing a new one.\" }\n            getInstance(context).removeSingleSubscription(\n                sdkDataWipeEventSubscriber,\n                SdkDataWipeEvent::class.java\n            )\n        }\n        brazelog(V) { \"Subscribing sdk data wipe subscriber\" }\n        sdkDataWipeEventSubscriber = IEventSubscriber<SdkDataWipeEvent> {\n            inAppMessageStack.clear()\n            carryoverInAppMessage = null\n            unregisteredInAppMessage = null\n        }.also {\n            getInstance(context).addSingleSynchronousSubscription(\n                it, SdkDataWipeEvent::class.java\n            )\n        }\n    }\n\n    /**\n     * Registers the in-app message manager, which will listen to and display incoming in-app messages. The\n     * current Activity is required in order to properly inflate and display the in-app message view.\n     *\n     * Important note: Every Activity must call [registerInAppMessageManager] in the onResume lifecycle\n     * method, otherwise in-app messages may be lost!\n     *\n     * This method also calls [BrazeInAppMessageManager.ensureSubscribedToInAppMessageEvents].\n     * To be sure that no in-app messages are lost, you should call [BrazeInAppMessageManager.ensureSubscribedToInAppMessageEvents] as early\n     * as possible in your app, preferably in your [Application.onCreate].\n     *\n     * @param activity The current Activity.\n     */\n    open fun registerInAppMessageManager(activity: Activity?) {\n        if (activity == null) {\n            brazelog(W) { \"Null Activity passed to registerInAppMessageManager. Doing nothing\" }\n            return\n        } else {\n            brazelog(V) { \"Registering InAppMessageManager with activity: ${activity.localClassName}\" }\n        }\n\n        // We need the current Activity so that we can inflate or programmatically create the in-app message\n        // View for each Activity. We cannot share the View because doing so would create a memory leak.\n        mActivity = activity\n        if (mApplicationContext == null) {\n            // Note, because this class is a singleton and doesn't have any dependencies passed in,\n            // we cache the application context here because it's not available (as it normally would be\n            // from Braze initialization).\n            mApplicationContext = activity.applicationContext\n            if (mApplicationContext == null) {\n                brazelog(W) { \"Activity had null applicationContext in registerInAppMessageManager. Doing Nothing.\" }\n                return\n            }\n        }\n        if (configurationProvider == null) {\n            configurationProvider = mApplicationContext?.let { BrazeConfigurationProvider(it) }\n        }\n\n        // We have a special check to see if the host app switched to a different Activity (or recreated\n        // the same Activity during an orientation change) so that we can redisplay the in-app message.\n        if (carryoverInAppMessage != null) {\n            carryoverInAppMessage?.let {\n                brazelog { \"Requesting display of carryover in-app message.\" }\n                it.animateIn = false\n                displayInAppMessage(it, true)\n            }\n            carryoverInAppMessage = null\n        } else {\n            unregisteredInAppMessage?.let {\n                brazelog { \"Adding previously unregistered in-app message.\" }\n                addInAppMessage(it)\n                unregisteredInAppMessage = null\n            }\n        }\n        mApplicationContext?.let { ensureSubscribedToInAppMessageEvents(it) }\n    }\n\n    /**\n     * Unregisters the in-app message manager.\n     *\n     * @param activity The current Activity.\n     */\n    open fun unregisterInAppMessageManager(activity: Activity?) {\n        if (shouldNextUnregisterBeSkipped) {\n            brazelog {\n                \"Skipping unregistration due to \" +\n                    \"setShouldNextUnregisterBeSkipped being true. Activity: ${activity?.localClassName}\"\n            }\n            shouldNextUnregisterBeSkipped = false\n            return\n        }\n        if (activity == null) {\n            // The activity is not needed to unregister so we can continue unregistration with it being null.\n            brazelog(W) { \"Null Activity passed to unregisterInAppMessageManager.\" }\n        } else {\n            brazelog(V) { \"Unregistering InAppMessageManager from activity: ${activity.localClassName}\" }\n        }\n\n        // If there is an in-app message being displayed when the host app transitions to another Activity (or\n        // requests an orientation change), we save it in memory so that we can redisplay it when the\n        // operation is done.\n        val viewWrapper = inAppMessageViewWrapper\n        if (viewWrapper != null) {\n            val inAppMessageView = viewWrapper.inAppMessageView\n            if (inAppMessageView is InAppMessageHtmlBaseView) {\n                brazelog { \"In-app message view includes HTML. Removing the page finished listener.\" }\n                inAppMessageView.setHtmlPageFinishedListener(null)\n            }\n            inAppMessageView.removeViewFromParent()\n\n            // Only continue if we're not animating a close\n            carryoverInAppMessage = if (viewWrapper.isAnimatingClose) {\n                // Note that mInAppMessageViewWrapper may be null after this call\n                inAppMessageViewLifecycleListener.afterClosed(viewWrapper.inAppMessage)\n                null\n            } else {\n                viewWrapper.inAppMessage\n            }\n            inAppMessageViewWrapper = null\n        } else {\n            carryoverInAppMessage = null\n        }\n        mActivity = null\n        displayingInAppMessage.set(false)\n    }\n\n    /**\n     * Provides a in-app message that will then be handled by the in-app message manager. If no in-app message is being\n     * displayed, it will attempt to display the in-app message immediately.\n     *\n     * @param inAppMessage The in-app message to add.\n     */\n    open fun addInAppMessage(inAppMessage: IInAppMessage?) {\n        if (inAppMessage != null) {\n            inAppMessageStack.push(inAppMessage)\n            requestDisplayInAppMessage()\n        }\n    }\n\n    /**\n     * Asks the InAppMessageManager to display the next in-app message if one is not currently being displayed.\n     * If one is being displayed, this method will return false and will not display the next in-app message.\n     *\n     * @return A boolean value indicating whether an asynchronous task to display the in-app message display was executed.\n     */\n    @Suppress(\"LongMethod\", \"ReturnCount\")\n    open fun requestDisplayInAppMessage(): Boolean {\n        return try {\n            if (mActivity == null) {\n                if (!inAppMessageStack.empty()) {\n                    brazelog(W) {\n                        \"No activity is currently registered to receive in-app messages. Saving in-app \" +\n                            \"message as unregistered in-app message. It will automatically be displayed \" +\n                            \"when the next activity registers to receive in-app messages.\"\n                    }\n                    unregisteredInAppMessage = inAppMessageStack.pop()\n                } else {\n                    brazelog {\n                        \"No activity is currently registered to receive in-app messages and the in-app \" +\n                            \"message stack is empty. Doing nothing.\"\n                    }\n                }\n                return false\n            }\n            if (displayingInAppMessage.get()) {\n                brazelog { \"A in-app message is currently being displayed. Ignoring request to display in-app message.\" }\n                return false\n            }\n            if (inAppMessageStack.isEmpty()) {\n                brazelog { \"The in-app message stack is empty. No in-app message will be displayed.\" }\n                return false\n            }\n            val inAppMessage = inAppMessageStack.pop()\n            val inAppMessageOperation: InAppMessageOperation = if (!inAppMessage.isControl) {\n                inAppMessageManagerListener.beforeInAppMessageDisplayed(inAppMessage)\n            } else {\n                brazelog { \"Using the control in-app message manager listener.\" }\n                controlInAppMessageManagerListener.beforeInAppMessageDisplayed(inAppMessage)\n            }\n            when (inAppMessageOperation) {\n                InAppMessageOperation.DISPLAY_NOW -> brazelog {\n                    \"The IInAppMessageManagerListener method beforeInAppMessageDisplayed returned DISPLAY_NOW. The \" +\n                        \"in-app message will be displayed.\"\n                }\n                InAppMessageOperation.DISPLAY_LATER -> {\n                    brazelog {\n                        \"The IInAppMessageManagerListener method beforeInAppMessageDisplayed returned DISPLAY_LATER. The \" +\n                            \"in-app message will be pushed back onto the stack.\"\n                    }\n                    inAppMessageStack.push(inAppMessage)\n                    return false\n                }\n                InAppMessageOperation.DISCARD -> {\n                    brazelog {\n                        \"The IInAppMessageManagerListener method beforeInAppMessageDisplayed returned DISCARD. The \" +\n                            \"in-app message will not be displayed and will not be put back on the stack.\"\n                    }\n                    return false\n                }\n            }\n            prepareInAppMessageForDisplay(inAppMessage)\n            true\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Error running requestDisplayInAppMessage\" }\n            false\n        }\n    }\n\n    /**\n     * Hides any currently displaying in-app message. Note that in-app message animation\n     * is configurable on the in-app message model itself and should be configured there.\n     *\n     * @param dismissed whether the message was dismissed by the user. If dismissed is true,\n     * IInAppMessageViewLifecycleListener.onDismissed() will be called on the current\n     * IInAppMessageViewLifecycleListener.\n     */\n    open fun hideCurrentlyDisplayingInAppMessage(dismissed: Boolean) {\n        shouldNextUnregisterBeSkipped = false\n        val inAppMessageWrapperView = inAppMessageViewWrapper\n        if (inAppMessageWrapperView != null) {\n            if (dismissed) {\n                inAppMessageViewLifecycleListener.onDismissed(\n                    inAppMessageWrapperView.inAppMessageView,\n                    inAppMessageWrapperView.inAppMessage\n                )\n            }\n            inAppMessageWrapperView.close()\n        }\n    }\n\n    /**\n     * Resets the [BrazeInAppMessageManager] to its original state before the last in-app message\n     * was displayed. This allows for a new in-app message to be displayed after calling this method.\n     * [ViewUtils.setActivityRequestedOrientation] is called with the original\n     * orientation before the last in-app message was displayed.\n     */\n    open fun resetAfterInAppMessageClose() {\n        brazelog(V) { \"Resetting after in-app message close.\" }\n        inAppMessageViewWrapper = null\n        val activity = mActivity\n        val origOrientation = originalOrientation\n        displayingInAppMessage.set(false)\n        if (activity != null && origOrientation != null) {\n            brazelog { \"Setting requested orientation to original orientation $origOrientation\" }\n            activity.setActivityRequestedOrientation(origOrientation)\n            originalOrientation = null\n        }\n    }\n\n    // For backwards compatibility\n    open fun getIsCurrentlyDisplayingInAppMessage() =\n        displayingInAppMessage.get()\n\n    /**\n     * Internal method, do not call as part of an integration!\n     *\n     * Attempts to display an [IInAppMessage] to the user.\n     *\n     * @param inAppMessage The [IInAppMessage].\n     * @param isCarryOver  If this [IInAppMessage] is \"carried over\" from an [Activity] transition.\n     */\n    @Suppress(\"LongMethod\", \"ComplexMethod\", \"NestedBlockDepth\", \"ThrowsCount\", \"TooGenericExceptionThrown\")\n    // The TooGenericExceptionThrown is here because some clients may have written code that's dependent\n    // on that very generic exception, so we want to stay backwards compatible.\n    open fun displayInAppMessage(inAppMessage: IInAppMessage, isCarryOver: Boolean) {\n        brazelog(V) {\n            \"Attempting to display in-app message with payload: ${inAppMessage.forJsonPut().getPrettyPrintedString()}\"\n        }\n\n        // Note: for displayingInAppMessage to be accurate it requires this method does not exit\n        // anywhere but the at the end of this try/catch when we know whether we are successfully\n        // displaying the in-app message or not.\n        if (!displayingInAppMessage.compareAndSet(false, true)) {\n            brazelog {\n                \"A in-app message is currently being displayed. Adding in-app message back on the stack.\"\n            }\n            inAppMessageStack.push(inAppMessage)\n            return\n        }\n        try {\n            val activity = mActivity\n            if (activity == null) {\n                carryoverInAppMessage = inAppMessage\n                throw Exception(\n                    \"No Activity is currently registered to receive in-app messages. Registering \" +\n                        \"in-app message as carry-over in-app message. It will automatically be \" +\n                        \"displayed when the next Activity registers to receive in-app messages.\"\n                )\n            }\n            if (!isCarryOver) {\n                val inAppMessageExpirationTimestamp = inAppMessage.expirationTimestamp\n                if (inAppMessageExpirationTimestamp > 0) {\n                    val currentTimeMillis = System.currentTimeMillis()\n                    if (currentTimeMillis > inAppMessageExpirationTimestamp) {\n                        throw Exception(\n                            \"In-app message is expired. Doing nothing. Expiration: \" +\n                                \"$inAppMessageExpirationTimestamp. Current time: $currentTimeMillis\"\n                        )\n                    }\n                } else {\n                    brazelog { \"Expiration timestamp not defined. Continuing.\" }\n                }\n            } else {\n                brazelog { \"Not checking expiration status for carry-over in-app message.\" }\n            }\n            if (!verifyOrientationStatus(inAppMessage)) {\n                // No display failure gets logged here since control in-app messages would also be affected.\n                throw Exception(\"Current orientation did not match specified orientation for in-app message. Doing nothing.\")\n            }\n\n            // At this point, the only factors that would inhibit in-app message display are view creation issues.\n            // Since control in-app messages have no view, this is the end of execution for control in-app messages\n            if (inAppMessage.isControl) {\n                brazelog {\n                    \"Not displaying control in-app message. Logging impression and ending display execution.\"\n                }\n                inAppMessage.logImpression()\n                resetAfterInAppMessageClose()\n                return\n            }\n            if (inAppMessage.containsInvalidBrazeAction()) {\n                val inAppMessageEvent = inAppMessageEventMap[inAppMessage]\n                brazelog(I) { \"Cannot show message containing an invalid Braze Action.\" }\n                if (inAppMessageEvent != null) {\n                    brazelog(I) { \"Attempting to perform any fallback actions.\" }\n                    retryInAppMessage(activity.applicationContext, inAppMessageEvent)\n                }\n                resetAfterInAppMessageClose()\n                return\n            }\n            if (inAppMessage.containsAnyPushPermissionBrazeActions()\n                && !activity.wouldPushPermissionPromptDisplay()\n            ) {\n                val inAppMessageEvent = inAppMessageEventMap[inAppMessage]\n                brazelog(I) {\n                    \"Cannot show message containing a Braze Actions Push Prompt due to existing \" +\n                        \"push prompt status, Android API version, or Target SDK level.\"\n                }\n                if (inAppMessageEvent != null) {\n                    brazelog(I) { \"Attempting to perform any fallback actions.\" }\n                    retryInAppMessage(activity.applicationContext, inAppMessageEvent)\n                }\n                resetAfterInAppMessageClose()\n                return\n            }\n            val inAppMessageViewFactory = getInAppMessageViewFactory(inAppMessage)\n            if (inAppMessageViewFactory == null) {\n                inAppMessage.logDisplayFailure(InAppMessageFailureType.DISPLAY_VIEW_GENERATION)\n                throw Exception(\"ViewFactory from getInAppMessageViewFactory was null.\")\n            }\n            val inAppMessageView = inAppMessageViewFactory.createInAppMessageView(\n                activity, inAppMessage\n            )\n            if (inAppMessageView == null) {\n                inAppMessage.logDisplayFailure(InAppMessageFailureType.DISPLAY_VIEW_GENERATION)\n                throw Exception(\n                    \"The in-app message view returned from the IInAppMessageViewFactory was null. \" +\n                        \"The in-app message will not be displayed and will not be put back on the stack.\"\n                )\n            }\n            if (inAppMessageView.parent != null) {\n                inAppMessage.logDisplayFailure(InAppMessageFailureType.DISPLAY_VIEW_GENERATION)\n                throw Exception(\n                    \"The in-app message view returned from the IInAppMessageViewFactory already has a parent. This \" +\n                        \"is a sign that the view is being reused. The IInAppMessageViewFactory method createInAppMessageView\" +\n                        \"must return a new view without a parent. The in-app message will not be displayed and will not \" +\n                        \"be put back on the stack.\"\n                )\n            }\n\n            val configProvider = configurationProvider\n                ?: throw Exception(\n                    \"configurationProvider is null. The in-app message will not be displayed and will not be\" +\n                        \"put back on the stack.\"\n                )\n            val openingAnimation = inAppMessageAnimationFactory.getOpeningAnimation(inAppMessage)\n            val closingAnimation = inAppMessageAnimationFactory.getClosingAnimation(inAppMessage)\n            val viewWrapperFactory = inAppMessageViewWrapperFactory\n            inAppMessageViewWrapper = when (inAppMessageView) {\n                is IInAppMessageImmersiveView -> {\n                    brazelog { \"Creating view wrapper for immersive in-app message.\" }\n                    val inAppMessageViewImmersive = inAppMessageView as IInAppMessageImmersiveView\n                    val inAppMessageImmersiveBase = inAppMessage as InAppMessageImmersiveBase\n                    val numButtons = inAppMessageImmersiveBase.messageButtons.size\n                    viewWrapperFactory.createInAppMessageViewWrapper(\n                        inAppMessageView,\n                        inAppMessage,\n                        inAppMessageViewLifecycleListener,\n                        configProvider,\n                        openingAnimation,\n                        closingAnimation,\n                        inAppMessageViewImmersive.messageClickableView,\n                        inAppMessageViewImmersive.getMessageButtonViews(numButtons),\n                        inAppMessageViewImmersive.messageCloseButtonView\n                    )\n                }\n                is IInAppMessageView -> {\n                    brazelog { \"Creating view wrapper for base in-app message.\" }\n                    val inAppMessageViewBase = inAppMessageView as IInAppMessageView\n                    viewWrapperFactory.createInAppMessageViewWrapper(\n                        inAppMessageView,\n                        inAppMessage,\n                        inAppMessageViewLifecycleListener,\n                        configProvider,\n                        openingAnimation,\n                        closingAnimation,\n                        inAppMessageViewBase.messageClickableView\n                    )\n                }\n                else -> {\n                    brazelog { \"Creating view wrapper for in-app message.\" }\n                    viewWrapperFactory.createInAppMessageViewWrapper(\n                        inAppMessageView,\n                        inAppMessage,\n                        inAppMessageViewLifecycleListener,\n                        configProvider,\n                        openingAnimation,\n                        closingAnimation,\n                        inAppMessageView\n                    )\n                }\n            }\n\n            val viewWrapper = inAppMessageViewWrapper\n\n            // If this message includes HTML, delay display until the content has finished loading\n            if (inAppMessageView is InAppMessageHtmlBaseView) {\n                brazelog {\n                    \"In-app message view includes HTML. Delaying display until the content has finished loading.\"\n                }\n                inAppMessageView.setHtmlPageFinishedListener {\n                    try {\n                        if (viewWrapper != null) {\n                            brazelog {\n                                \"Page has finished loading. Opening in-app message view wrapper.\"\n                            }\n                            viewWrapper.open(activity)\n                        }\n                    } catch (e: Exception) {\n                        brazelog(E, e) { \"Failed to open view wrapper in page finished listener\" }\n                    }\n                }\n            } else {\n                viewWrapper?.open(activity)\n            }\n        } catch (e: Throwable) {\n            brazelog(E, e) {\n                \"Could not display in-app message with payload: ${inAppMessage.forJsonPut().getPrettyPrintedString()}\"\n            }\n            resetAfterInAppMessageClose()\n        }\n    }\n\n    private fun createInAppMessageEventSubscriber(): IEventSubscriber<InAppMessageEvent> {\n        return IEventSubscriber { event: InAppMessageEvent ->\n            val inAppMessage = event.inAppMessage\n            inAppMessageEventMap[inAppMessage] = event\n            addInAppMessage(inAppMessage)\n        }\n    }\n\n    /**\n     * For in-app messages that have a preferred orientation, locks the screen orientation and\n     * returns true if the screen is currently in the preferred orientation. If the screen is not\n     * currently in the preferred orientation, returns false.\n     *\n     * Always returns true for tablets, regardless of current orientation.\n     *\n     * Always returns true if the in-app message doesn't have a preferred orientation.\n     */\n    @SuppressLint(\"InlinedApi\")\n    @VisibleForTesting\n    open fun verifyOrientationStatus(inAppMessage: IInAppMessage): Boolean {\n        val activity = mActivity\n        val preferredOrientation = inAppMessage.orientation\n\n        if (activity == null) {\n            brazelog(W) { \"Cannot verify orientation status with null Activity.\" }\n        } else if (activity.isRunningOnTablet()) {\n            brazelog { \"Running on tablet. In-app message can be displayed in any orientation.\" }\n        } else if (preferredOrientation === Orientation.ANY) {\n            brazelog { \"Any orientation specified. In-app message can be displayed in any orientation.\" }\n        } else {\n            val currentScreenOrientation = activity.resources.configuration.orientation\n            return if (isCurrentOrientationValid(currentScreenOrientation, preferredOrientation)) {\n                if (originalOrientation == null) {\n                    brazelog { \"Requesting orientation lock.\" }\n                    originalOrientation = activity.requestedOrientation\n                    // This constant was introduced in API 18, so for devices pre 18 this will be a no-op\n                    activity.setActivityRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED)\n                }\n                true\n            } else {\n                false\n            }\n        }\n        return true\n    }\n\n    companion object {\n        private val instanceLock = ReentrantLock()\n\n        @Volatile\n        private var instance: BrazeInAppMessageManager? = null\n\n        @JvmStatic\n        fun getInstance(): BrazeInAppMessageManager {\n            if (instance != null) {\n                return instance as BrazeInAppMessageManager\n            }\n            instanceLock.withLock {\n                if (instance == null) {\n                    instance = BrazeInAppMessageManager()\n                }\n            }\n            return instance as BrazeInAppMessageManager\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/DefaultInAppMessageViewWrapper.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.app.Activity\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.animation.Animation\nimport android.widget.FrameLayout\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsCompat\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.inappmessage.DismissType\nimport com.braze.enums.inappmessage.MessageType\nimport com.braze.enums.inappmessage.SlideFrom\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.models.inappmessage.InAppMessageSlideup\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.listeners.IInAppMessageViewLifecycleListener\nimport com.braze.ui.inappmessage.listeners.SwipeDismissTouchListener.DismissCallbacks\nimport com.braze.ui.inappmessage.listeners.TouchAwareSwipeDismissTouchListener\nimport com.braze.ui.inappmessage.listeners.TouchAwareSwipeDismissTouchListener.ITouchListener\nimport com.braze.ui.inappmessage.views.IInAppMessageImmersiveView\nimport com.braze.ui.inappmessage.views.IInAppMessageView\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlBaseView\nimport com.braze.ui.support.isDeviceNotInTouchMode\nimport com.braze.ui.support.removeViewFromParent\nimport com.braze.ui.support.setFocusableInTouchModeAndRequestFocus\n\n/**\n * Constructor for base and slideup view wrappers. Adds click listeners to the in-app message view and\n * adds swipe functionality to slideup in-app messages.\n *\n * @param inAppMessageView                  In-app message top level view.\n * @param inAppMessage                      In-app message model.\n * @param inAppMessageViewLifecycleListener In-app message lifecycle listener.\n * @param configurationProvider       Configuration provider.\n * @param clickableInAppMessageView         View for which click actions apply.\n * @param buttonViews                       List of views corresponding to MessageButton objects stored in the in-app message model object.\n * These views should map one to one with the MessageButton objects.\n * @param closeButton                       The [View] responsible for closing the in-app message.\n */\n@Suppress(\"TooManyFunctions\")\nopen class DefaultInAppMessageViewWrapper @JvmOverloads constructor(\n    override val inAppMessageView: View,\n    override val inAppMessage: IInAppMessage,\n    open val inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener,\n    open val configurationProvider: BrazeConfigurationProvider,\n    open val openingAnimation: Animation?,\n    open val closingAnimation: Animation?,\n    open var clickableInAppMessageView: View?,\n    open var buttonViews: List<View>? = null,\n    open var closeButton: View? = null\n) : IInAppMessageViewWrapper {\n    @Suppress(\"deprecation\")\n    open val inAppMessageCloser: InAppMessageCloser\n    override var isAnimatingClose = false\n    open var dismissRunnable: Runnable? = null\n\n    /**\n     * The [View] that previously held focus before a message is displayed as\n     * given via [Activity.getCurrentFocus].\n     */\n    open var previouslyFocusedView: View? = null\n\n    /**\n     * A mapping of the view accessibility flags of views before overriding them.\n     * Used in conjunction with [com.braze.configuration.BrazeConfig.Builder.setIsInAppMessageAccessibilityExclusiveModeEnabled]\n     */\n    open var viewAccessibilityFlagMap = HashMap<Int, Int>()\n\n    /**\n     * The [ViewGroup] parent of the in-app message.\n     */\n    open var contentViewGroupParentLayout: ViewGroup? = null\n\n    init {\n        clickableInAppMessageView = clickableInAppMessageView ?: inAppMessageView\n\n        // Only slideup in-app messages can be swiped.\n        if (inAppMessage is InAppMessageSlideup) {\n            // Adds the swipe listener to the in-app message View. All slideup in-app messages should be dismissible via a swipe\n            // (even auto close slideup in-app messages).\n            val dismissCallbacks = createDismissCallbacks()\n            val touchAwareSwipeListener = TouchAwareSwipeDismissTouchListener(\n                inAppMessageView, dismissCallbacks\n            )\n            // We set a custom touch listener that cancel the auto close runnable when touched and adds\n            // a new runnable when the touch ends.\n            touchAwareSwipeListener.setTouchListener(createTouchAwareListener())\n            clickableInAppMessageView?.setOnTouchListener(touchAwareSwipeListener)\n        }\n        clickableInAppMessageView?.setOnClickListener(createClickListener())\n        @Suppress(\"deprecation\")\n        inAppMessageCloser = InAppMessageCloser(this)\n\n        this.closeButton?.setOnClickListener(createCloseInAppMessageClickListener())\n        this.buttonViews?.forEach { it.setOnClickListener(createButtonClickListener()) }\n    }\n\n    override fun open(activity: Activity) {\n        brazelog(V) { \"Opening in-app message view wrapper\" }\n        // Retrieve the ViewGroup which will display the in-app message\n        val parentViewGroup = getParentViewGroup(activity)\n        val parentViewGroupHeight = parentViewGroup.height\n        if (configurationProvider.isInAppMessageAccessibilityExclusiveModeEnabled) {\n            contentViewGroupParentLayout = parentViewGroup\n            viewAccessibilityFlagMap.clear()\n            setAllViewGroupChildrenAsNonAccessibilityImportant(\n                contentViewGroupParentLayout,\n                viewAccessibilityFlagMap\n            )\n        }\n        previouslyFocusedView = activity.currentFocus\n\n        // If the parent ViewGroup's height is 0, that implies it hasn't been drawn yet. We add a\n        // ViewTreeObserver to wait until its drawn so we can get a proper measurement.\n        if (parentViewGroupHeight == 0) {\n            parentViewGroup.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {\n                override fun onLayoutChange(\n                    view: View,\n                    left: Int,\n                    top: Int,\n                    right: Int,\n                    bottom: Int,\n                    oldLeft: Int,\n                    oldTop: Int,\n                    oldRight: Int,\n                    oldBottom: Int\n                ) {\n                    parentViewGroup.removeOnLayoutChangeListener(this)\n                    brazelog { \"Detected (bottom - top) of ${bottom - top} in OnLayoutChangeListener\" }\n                    parentViewGroup.removeView(inAppMessageView)\n                    parentViewGroup.post {\n                        addInAppMessageViewToViewGroup(\n                            parentViewGroup,\n                            inAppMessage,\n                            inAppMessageView,\n                            inAppMessageViewLifecycleListener\n                        )\n                    }\n                }\n            })\n        } else {\n            brazelog { \"Detected root view height of $parentViewGroupHeight\" }\n            addInAppMessageViewToViewGroup(\n                parentViewGroup,\n                inAppMessage,\n                inAppMessageView,\n                inAppMessageViewLifecycleListener\n            )\n        }\n    }\n\n    override fun close() {\n        if (configurationProvider.isInAppMessageAccessibilityExclusiveModeEnabled) {\n            resetAllViewGroupChildrenToPreviousAccessibilityFlagOrAuto(\n                contentViewGroupParentLayout,\n                viewAccessibilityFlagMap\n            )\n        }\n        inAppMessageView.removeCallbacks(dismissRunnable)\n        inAppMessageViewLifecycleListener.beforeClosed(inAppMessageView, inAppMessage)\n        if (inAppMessage.animateOut) {\n            isAnimatingClose = true\n            setAndStartAnimation(false)\n        } else {\n            closeInAppMessageView()\n        }\n    }\n\n    /**\n     * Gets the [ViewGroup] which will display the in-app message. Note that\n     * if this implementation is overridden, then\n     * [DefaultInAppMessageViewWrapper.getLayoutParams] should\n     * also most likely be overridden to match the [ViewGroup] subclass\n     * returned here.\n     */\n    open fun getParentViewGroup(activity: Activity): ViewGroup =\n        // The android.R.id.content {@link FrameLayout} contains the\n        // {@link Activity}'s top-level layout as its first child.\n        activity.window.decorView.findViewById(android.R.id.content)\n\n    /**\n     * Creates the [ViewGroup.LayoutParams] used for adding the\n     * [IInAppMessageView] to the [ViewGroup] returned by\n     * [DefaultInAppMessageViewWrapper.getParentViewGroup].\n     *\n     * Note that the exact subclass of [ViewGroup.LayoutParams] should\n     * match that of the [ViewGroup] returned by\n     * [DefaultInAppMessageViewWrapper.getParentViewGroup].\n     */\n    open fun getLayoutParams(inAppMessage: IInAppMessage?): ViewGroup.LayoutParams {\n        val layoutParams = FrameLayout.LayoutParams(\n            FrameLayout.LayoutParams.MATCH_PARENT,\n            FrameLayout.LayoutParams.WRAP_CONTENT\n        )\n        if (inAppMessage is InAppMessageSlideup) {\n            layoutParams.gravity =\n                if (inAppMessage.slideFrom === SlideFrom.TOP) Gravity.TOP else Gravity.BOTTOM\n        }\n        return layoutParams\n    }\n\n    /**\n     * Adds the [IInAppMessageView] to the parent [ViewGroup]. Also\n     * calls [IInAppMessageViewLifecycleListener.beforeOpened] and\n     * [IInAppMessageViewLifecycleListener.afterOpened].\n     */\n    open fun addInAppMessageViewToViewGroup(\n        parentViewGroup: ViewGroup,\n        inAppMessage: IInAppMessage,\n        inAppMessageView: View,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener\n    ) {\n        inAppMessageViewLifecycleListener.beforeOpened(inAppMessageView, inAppMessage)\n        brazelog { \"Adding In-app message view to parent view group.\" }\n        parentViewGroup.addView(inAppMessageView, getLayoutParams(inAppMessage))\n        if (inAppMessageView is IInAppMessageView) {\n            ViewCompat.requestApplyInsets(parentViewGroup)\n            ViewCompat.setOnApplyWindowInsetsListener(parentViewGroup) { _: View?, insets: WindowInsetsCompat? ->\n                if (insets == null) {\n                    // No margin fixing can be done with a null window inset\n                    return@setOnApplyWindowInsetsListener insets\n                }\n                val castInAppMessageView = inAppMessageView as IInAppMessageView\n                if (!castInAppMessageView.hasAppliedWindowInsets) {\n                    brazelog(V) { \"Calling applyWindowInsets on in-app message view.\" }\n                    castInAppMessageView.applyWindowInsets(insets)\n                } else {\n                    brazelog { \"Not reapplying window insets to in-app message view.\" }\n                }\n                insets\n            }\n        }\n        if (inAppMessage.animateIn) {\n            brazelog { \"In-app message view will animate into the visible area.\" }\n            setAndStartAnimation(true)\n            // The afterOpened lifecycle method gets called when the opening animation ends.\n        } else {\n            brazelog { \"In-app message view will be placed instantly into the visible area.\" }\n            // There is no opening animation, so we call the afterOpened lifecycle method immediately.\n            if (inAppMessage.dismissType === DismissType.AUTO_DISMISS) {\n                addDismissRunnable()\n            }\n            finalizeViewBeforeDisplay(inAppMessage, inAppMessageView, inAppMessageViewLifecycleListener)\n        }\n    }\n\n    /**\n     * Calls [View.announceForAccessibility] with the [IInAppMessage.getMessage]\n     * if the [IInAppMessageView] is [IInAppMessageImmersiveView] or the fallback message\n     * [IInAppMessageView] is a [InAppMessageHtmlBaseView].\n     */\n    open fun announceForAccessibilityIfNecessary(fallbackAccessibilityMessage: String? = \"In app message displayed.\") {\n        if (inAppMessageView is IInAppMessageImmersiveView) {\n            val message = inAppMessage.message\n            if (inAppMessage is IInAppMessageImmersive) {\n                // Announce the header and message together with a brief pause between them\n                val header = (inAppMessage as IInAppMessageImmersive).header\n                inAppMessageView.announceForAccessibility(\"$header . $message\")\n            } else {\n                inAppMessageView.announceForAccessibility(message)\n            }\n        } else if (inAppMessageView is InAppMessageHtmlBaseView) {\n            inAppMessageView.announceForAccessibility(fallbackAccessibilityMessage)\n        }\n    }\n\n    /**\n     * Creates a [View.OnClickListener] that calls\n     * [IInAppMessageViewLifecycleListener.onClicked].\n     *\n     * [IInAppMessageViewLifecycleListener.onClicked] is called and\n     * can be used to turn off the close animation. Full and modal in-app messages can\n     * only be clicked directly when they do not contain buttons.\n     * Slideup in-app messages are always clickable.\n     */\n    open fun createClickListener(): View.OnClickListener {\n        return View.OnClickListener {\n            if ((inAppMessage as? IInAppMessageImmersive)?.messageButtons?.isEmpty() == true\n                || inAppMessage !is IInAppMessageImmersive\n            ) {\n                inAppMessageViewLifecycleListener.onClicked(\n                    inAppMessageCloser,\n                    inAppMessageView,\n                    inAppMessage\n                )\n            }\n        }\n    }\n\n    /**\n     * @return A click listener that calls [IInAppMessageViewLifecycleListener.onButtonClicked]\n     * if the clicked [View.getId] matches that of a [MessageButton]'s [View].\n     */\n    open fun createButtonClickListener(): View.OnClickListener {\n        return View.OnClickListener { view: View ->\n            // The onClicked lifecycle method is called and it can be used to turn off the close animation.\n            val inAppMessageImmersive = inAppMessage as IInAppMessageImmersive\n            if (inAppMessageImmersive.messageButtons.isEmpty()) {\n                brazelog {\n                    \"Cannot create button click listener since this in-app message does not have message buttons.\"\n                }\n                return@OnClickListener\n            }\n            var index = 0\n            buttonViews?.let {\n                while (index < it.size) {\n                    if (view.id == it[index].id) {\n                        val messageButton = inAppMessageImmersive.messageButtons[index]\n                        inAppMessageViewLifecycleListener.onButtonClicked(\n                            inAppMessageCloser,\n                            messageButton,\n                            inAppMessageImmersive\n                        )\n                        return@OnClickListener\n                    }\n                    index++\n                }\n            }\n        }\n    }\n\n    open fun createCloseInAppMessageClickListener(): View.OnClickListener {\n        return View.OnClickListener {\n            BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(true)\n        }\n    }\n\n    open fun addDismissRunnable() {\n        if (dismissRunnable == null) {\n            dismissRunnable = Runnable {\n                BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(true)\n            }\n            inAppMessageView.postDelayed(\n                dismissRunnable,\n                inAppMessage.durationInMilliseconds.toLong()\n            )\n        }\n    }\n\n    /**\n     * Instantiates and executes the correct animation for the current in-app message. Slideup-type\n     * messages slide in from the top or bottom of the view. Other in-app messages fade in\n     * and out of view.\n     *\n     */\n    open fun setAndStartAnimation(opening: Boolean) {\n        val animation: Animation? = if (opening) {\n            openingAnimation\n        } else {\n            closingAnimation\n        }\n        animation?.setAnimationListener(createAnimationListener(opening))\n        inAppMessageView.clearAnimation()\n        inAppMessageView.animation = animation\n        animation?.startNow()\n        inAppMessageView.invalidate()\n    }\n\n    /**\n     * Closes the in-app message view.\n     * In this order, the following actions are performed:\n     *\n     * The view is removed from the parent.\n     * Any WebViews are explicitly paused or frame execution finished in some way.\n     * [IInAppMessageViewLifecycleListener.afterClosed] is called.\n     */\n    open fun closeInAppMessageView() {\n        brazelog { \"Closing in-app message view\" }\n        inAppMessageView.removeViewFromParent()\n        // In the case of HTML in-app messages, we need to make sure the\n        // WebView stops once the in-app message is removed.\n        (inAppMessageView as? InAppMessageHtmlBaseView)?.finishWebViewDisplay()\n\n        // Return the focus before closing the message\n        if (previouslyFocusedView != null) {\n            brazelog { \"Returning focus to view after closing message. View: $previouslyFocusedView\" }\n            previouslyFocusedView?.requestFocus()\n        }\n        inAppMessageViewLifecycleListener.afterClosed(inAppMessage)\n    }\n\n    /**\n     * Performs any last actions before calling\n     * [IInAppMessageViewLifecycleListener.beforeOpened].\n     */\n    open fun finalizeViewBeforeDisplay(\n        inAppMessage: IInAppMessage,\n        inAppMessageView: View,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener\n    ) {\n        if (isDeviceNotInTouchMode(inAppMessageView)) {\n            // Special behavior usual to TV environments\n\n            // For views with defined directional\n            // behavior, don't steal focus from them\n            when (inAppMessage.messageType) {\n                MessageType.MODAL, MessageType.FULL -> {}\n                else -> inAppMessageView.setFocusableInTouchModeAndRequestFocus()\n            }\n        } else {\n            inAppMessageView.setFocusableInTouchModeAndRequestFocus()\n        }\n        announceForAccessibilityIfNecessary()\n        inAppMessageViewLifecycleListener.afterOpened(inAppMessageView, inAppMessage)\n    }\n\n    open fun createDismissCallbacks(): DismissCallbacks {\n        return object : DismissCallbacks {\n            override fun canDismiss(token: Any?): Boolean = true\n\n            override fun onDismiss(view: View, token: Any?) {\n                inAppMessage.animateOut = false\n                BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(true)\n            }\n        }\n    }\n\n    open fun createTouchAwareListener(): ITouchListener {\n        return object : ITouchListener {\n            override fun onTouchStartedOrContinued() {\n                inAppMessageView.removeCallbacks(dismissRunnable)\n            }\n\n            override fun onTouchEnded() {\n                if (inAppMessage.dismissType === DismissType.AUTO_DISMISS) {\n                    addDismissRunnable()\n                }\n            }\n        }\n    }\n\n    open fun createAnimationListener(opening: Boolean): Animation.AnimationListener {\n        return if (opening) {\n            object : Animation.AnimationListener {\n                override fun onAnimationStart(animation: Animation?) {}\n\n                // This lifecycle callback has been observed to not be called during slideup animations\n                // on occasion. Do not add any code that *MUST* be executed here.\n                override fun onAnimationEnd(animation: Animation?) {\n                    if (inAppMessage.dismissType === DismissType.AUTO_DISMISS) {\n                        addDismissRunnable()\n                    }\n                    brazelog { \"In-app message animated into view.\" }\n                    finalizeViewBeforeDisplay(\n                        inAppMessage,\n                        inAppMessageView,\n                        inAppMessageViewLifecycleListener\n                    )\n                }\n\n                override fun onAnimationRepeat(animation: Animation?) {}\n            }\n        } else {\n            object : Animation.AnimationListener {\n                override fun onAnimationStart(animation: Animation?) {}\n                override fun onAnimationEnd(animation: Animation?) {\n                    inAppMessageView.clearAnimation()\n                    inAppMessageView.visibility = View.GONE\n                    closeInAppMessageView()\n                }\n\n                override fun onAnimationRepeat(animation: Animation?) {}\n            }\n        }\n    }\n\n    companion object {\n        /**\n         * Sets all [View] children of the [ViewGroup]\n         * as [ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS].\n         */\n        fun setAllViewGroupChildrenAsNonAccessibilityImportant(\n            viewGroup: ViewGroup?,\n            viewAccessibilityFlagMap: MutableMap<Int, Int>\n        ) {\n            if (viewGroup == null) {\n                brazelog(W) {\n                    \"In-app message ViewGroup was null. Not preparing in-app message accessibility for exclusive mode.\"\n                }\n                return\n            }\n            for (i in 0 until viewGroup.childCount) {\n                val child = viewGroup.getChildAt(i)\n                if (child != null) {\n                    viewAccessibilityFlagMap[child.id] = child.importantForAccessibility\n                    ViewCompat.setImportantForAccessibility(\n                        child,\n                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS\n                    )\n                }\n            }\n        }\n\n        /**\n         * Sets all [View] children of the [ViewGroup] as their previously\n         * mapped accessibility flag, or [ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO] if\n         * not found in the mapping.\n         */\n        @Suppress(\"NestedBlockDepth\", \"FunctionMaxLength\")\n        fun resetAllViewGroupChildrenToPreviousAccessibilityFlagOrAuto(\n            viewGroup: ViewGroup?,\n            viewAccessibilityFlagMap: Map<Int, Int>\n        ) {\n            if (viewGroup == null) {\n                brazelog(W) {\n                    \"In-app message ViewGroup was null. Not resetting in-app message accessibility for exclusive mode.\"\n                }\n                return\n            }\n            for (i in 0 until viewGroup.childCount) {\n                val child = viewGroup.getChildAt(i)\n                if (child != null) {\n                    val id = child.id\n                    if (viewAccessibilityFlagMap.containsKey(id)) {\n                        viewAccessibilityFlagMap[id]?.let {\n                            ViewCompat.setImportantForAccessibility(\n                                child,\n                                it\n                            )\n                        }\n                    } else {\n                        ViewCompat.setImportantForAccessibility(\n                            child,\n                            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/IInAppMessageAnimationFactory.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.view.animation.Animation\nimport com.braze.models.inappmessage.IInAppMessage\n\ninterface IInAppMessageAnimationFactory {\n    /**\n     * This method returns the animation that will be used to animate the message as it enters the screen.\n     * @return animation that will be applied to the in-app message view using\n     * [android.view.View.setAnimation]\n     */\n    fun getOpeningAnimation(inAppMessage: IInAppMessage): Animation?\n\n    /**\n     * This method returns the animation that will be used to animate the message as it exits the screen.\n     * @return animation that will be applied to the in-app message view using\n     * [android.view.View.setAnimation]\n     */\n    fun getClosingAnimation(inAppMessage: IInAppMessage): Animation?\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/IInAppMessageViewFactory.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.app.Activity\nimport android.view.View\nimport com.braze.models.inappmessage.IInAppMessage\n\ninterface IInAppMessageViewFactory {\n    /**\n     * This method should either inflate or programmatically create a new View that will be used\n     * to display an in-app message. A new View must be created on every call. This\n     * prevents the memory leak that would occur if the View was shared by multiple Activity\n     * classes.\n     *\n     * Note: Do not add click/touch listeners directly to the view. They will be ignored. Instead,\n     * use an []IInAppMessageManagerListener] to perform custom logic in response to user action.\n     *\n     * @return View that will be used to display the in-app message.\n     */\n    fun createInAppMessageView(activity: Activity, inAppMessage: IInAppMessage): View?\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/IInAppMessageViewWrapper.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.app.Activity\nimport android.view.View\nimport com.braze.models.inappmessage.IInAppMessage\n\ninterface IInAppMessageViewWrapper {\n    /**\n     * @return The [View] representing the [IInAppMessage]\n     * that is visible to the user.\n     */\n    val inAppMessageView: View\n\n    /**\n     * @return The [IInAppMessage] being wrapped.\n     */\n    val inAppMessage: IInAppMessage\n\n    /**\n     * @return Whether the [IInAppMessage] view is\n     * currently in the process of its close animation.\n     */\n    val isAnimatingClose: Boolean\n\n    /**\n     * Opens an [IInAppMessage] on the [Activity]. As a\n     * result of this call, it is expected that an [IInAppMessage]\n     * is visible and interactable by the user.\n     *\n     * Note that this method is expected to be called\n     * on the main UI thread and should run synchronously.\n     */\n    fun open(activity: Activity)\n\n    /**\n     * Closes an [IInAppMessage]. As a\n     * result of this call, it is expected that an [IInAppMessage]\n     * is no longer visible and not interactable by the user.\n     *\n     * Note that this method is expected to be called\n     * on the main UI thread and should run synchronously.\n     */\n    fun close()\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/IInAppMessageViewWrapperFactory.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.view.View\nimport android.view.animation.Animation\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.ui.inappmessage.listeners.IInAppMessageViewLifecycleListener\n\ninterface IInAppMessageViewWrapperFactory {\n    /**\n     * Factory interface for non [IInAppMessageImmersive] view wrappers.\n     * Implementations should add click listeners to the in-app message view and\n     * also add swipe functionality to [InAppMessageSlideup] in-app messages.\n     *\n     * @param inAppMessageView                  In-app message top level view visible to the user.\n     * @param inAppMessage                      In-app message model.\n     * @param inAppMessageViewLifecycleListener In-app message lifecycle listener.\n     * @param configurationProvider       Configuration provider.\n     * @param openingAnimation                  The [Animation] used when opening the [IInAppMessage]\n     * and becoming visible to the user.\n     * Should be called during [IInAppMessageViewWrapper.open].\n     * @param closingAnimation                  The [Animation] used when closing the [IInAppMessage].\n     * Should be called during [IInAppMessageViewWrapper.close].\n     * @param clickableInAppMessageView         [View] for which click actions apply.\n     */\n    @Suppress(\"LongParameterList\")\n    fun createInAppMessageViewWrapper(\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener,\n        configurationProvider: BrazeConfigurationProvider,\n        openingAnimation: Animation?,\n        closingAnimation: Animation?,\n        clickableInAppMessageView: View?\n    ): IInAppMessageViewWrapper\n\n    /**\n     * Constructor for [IInAppMessageImmersive] in-app message view wrappers.\n     * Implementations should add click listeners to the in-app message view and also\n     * add listeners to an optional close button and message button views.\n     *\n     * @param inAppMessageView                  In-app message top level view visible to the user.\n     * @param inAppMessage                      In-app message model.\n     * @param inAppMessageViewLifecycleListener In-app message lifecycle listener.\n     * @param configurationProvider       Configuration provider.\n     * @param openingAnimation                  The [Animation] used when opening the [IInAppMessage]\n     * and becoming visible to the user.\n     * Should be called during [IInAppMessageViewWrapper.open].\n     * @param closingAnimation                  The [Animation] used when closing the [IInAppMessage].\n     * Should be called during [IInAppMessageViewWrapper.close].\n     * @param clickableInAppMessageView         [View] for which click actions apply.\n     * @param buttons                           List of views corresponding to [MessageButton]\n     * objects stored in the in-app message model object.\n     * These views should map one to one with the MessageButton objects.\n     * @param closeButton                       The [View] responsible for closing the in-app message.\n     */\n    @Suppress(\"LongParameterList\")\n    fun createInAppMessageViewWrapper(\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener,\n        configurationProvider: BrazeConfigurationProvider,\n        openingAnimation: Animation?,\n        closingAnimation: Animation?,\n        clickableInAppMessageView: View?,\n        buttons: List<View>?,\n        closeButton: View?\n    ): IInAppMessageViewWrapper\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/InAppMessageCloser.java",
    "content": "package com.braze.ui.inappmessage;\n\nimport com.braze.models.inappmessage.IInAppMessage;\n\n/**\n * @deprecated Please use {@link BrazeInAppMessageManager#hideCurrentlyDisplayingInAppMessage}\n *             and {@link IInAppMessage#setAnimateOut}\n * A delegate method class used to close the currently displayed in-app message.\n */\n@Deprecated\npublic class InAppMessageCloser {\n  private final IInAppMessageViewWrapper mInAppMessageViewWrapper;\n\n  public InAppMessageCloser(IInAppMessageViewWrapper inAppMessageViewWrapper) {\n    mInAppMessageViewWrapper = inAppMessageViewWrapper;\n  }\n\n  public void close(boolean animate) {\n    mInAppMessageViewWrapper.getInAppMessage().setAnimateOut(animate);\n    mInAppMessageViewWrapper.close();\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/InAppMessageManagerBase.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport android.app.Activity\nimport android.content.Context\nimport androidx.annotation.RestrictTo\nimport com.braze.enums.inappmessage.MessageType\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageAnimationFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageFullViewFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageHtmlFullViewFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageHtmlViewFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageModalViewFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageSlideupViewFactory\nimport com.braze.ui.inappmessage.factories.DefaultInAppMessageViewWrapperFactory\nimport com.braze.ui.inappmessage.listeners.DefaultHtmlInAppMessageActionListener\nimport com.braze.ui.inappmessage.listeners.DefaultInAppMessageManagerListener\nimport com.braze.ui.inappmessage.listeners.DefaultInAppMessageWebViewClientListener\nimport com.braze.ui.inappmessage.listeners.IHtmlInAppMessageActionListener\nimport com.braze.ui.inappmessage.listeners.IInAppMessageManagerListener\nimport com.braze.ui.inappmessage.listeners.IInAppMessageWebViewClientListener\n\n@Suppress(\"TooManyFunctions\")\nopen class InAppMessageManagerBase {\n    open val doesClickOutsideModalViewDismissInAppMessageView\n        get() = doesClickOutsideModalViewDismissInAppMessageViewField\n\n    private var doesClickOutsideModalViewDismissInAppMessageViewField = false\n\n    /**\n     * Determines whether the next call to\n     * [BrazeInAppMessageManager.unregisterInAppMessageManager] will be ignored.\n     */\n    open var shouldNextUnregisterBeSkipped = false\n        set(shouldSkip) {\n            brazelog { \"Setting setShouldNextUnregisterBeSkipped to $shouldSkip\" }\n            field = shouldSkip\n        }\n\n    open val doesBackButtonDismissInAppMessageView\n        get() = doesBackButtonDismissInAppMessageViewField\n\n    private var doesBackButtonDismissInAppMessageViewField = true\n\n    // Since many clients have written code against this, we want to maintain backwards compatibility\n    // as much as possible. That's why we have these names and present them as JvmFields.\n    @JvmField\n    @Suppress(\"VariableNaming\")\n    protected var mActivity: Activity? = null\n\n    @JvmField\n    @Suppress(\"VariableNaming\")\n    protected var mApplicationContext: Context? = null\n\n    // These serve the purpose of allowing people to write more Kotlin-friendly code, but also provide\n    // the getActivity() and getApplicationContext() Java generated code for backwards compatibility\n    open val activity\n        get() = mActivity\n\n    open val applicationContext\n        get() = mApplicationContext\n\n    // view listeners\n    private val inAppMessageWebViewClientListener: IInAppMessageWebViewClientListener =\n        DefaultInAppMessageWebViewClientListener()\n\n    // html action listeners\n    private val defaultHtmlInAppMessageActionListener: IHtmlInAppMessageActionListener =\n        DefaultHtmlInAppMessageActionListener()\n\n    // factories\n    private val inAppMessageSlideupViewFactory: IInAppMessageViewFactory =\n        DefaultInAppMessageSlideupViewFactory()\n    private val inAppMessageModalViewFactory: IInAppMessageViewFactory =\n        DefaultInAppMessageModalViewFactory()\n    private val inAppMessageFullViewFactory: IInAppMessageViewFactory =\n        DefaultInAppMessageFullViewFactory()\n    private val inAppMessageHtmlFullViewFactory: IInAppMessageViewFactory =\n        DefaultInAppMessageHtmlFullViewFactory(inAppMessageWebViewClientListener)\n    private val inAppMessageHtmlViewFactory: IInAppMessageViewFactory =\n        DefaultInAppMessageHtmlViewFactory(inAppMessageWebViewClientListener)\n\n    // animation factory\n    private val inAppMessageAnimationFactoryField: IInAppMessageAnimationFactory =\n        DefaultInAppMessageAnimationFactory()\n\n    // manager listeners\n    private val defaultInAppMessageManagerListener: IInAppMessageManagerListener =\n        DefaultInAppMessageManagerListener()\n\n    // view wrapper factory\n    private val defaultInAppMessageViewWrapperFactory: IInAppMessageViewWrapperFactory =\n        DefaultInAppMessageViewWrapperFactory()\n\n    // custom listeners\n    private var customInAppMessageViewFactory: IInAppMessageViewFactory? = null\n    private var customInAppMessageAnimationFactory: IInAppMessageAnimationFactory? = null\n    private var customInAppMessageManagerListener: IInAppMessageManagerListener? = null\n    private var customInAppMessageViewWrapperFactory: IInAppMessageViewWrapperFactory? = null\n    private var customHtmlInAppMessageActionListener: IHtmlInAppMessageActionListener? = null\n\n    /**\n     * A custom listener to be fired for control in-app messages.\n     *\n     *\n     * see [IInAppMessage.isControl]\n     */\n    private var customControlInAppMessageManagerListener: IInAppMessageManagerListener? = null\n    open val inAppMessageManagerListener: IInAppMessageManagerListener\n        get() = customInAppMessageManagerListener ?: defaultInAppMessageManagerListener\n\n    /**\n     * A [IInAppMessageManagerListener] to be used only for control in-app messages.\n     *\n     * see [IInAppMessage.isControl]\n     */\n    open val controlInAppMessageManagerListener: IInAppMessageManagerListener\n        get() = customControlInAppMessageManagerListener ?: defaultInAppMessageManagerListener\n    open val htmlInAppMessageActionListener: IHtmlInAppMessageActionListener\n        get() = customHtmlInAppMessageActionListener ?: defaultHtmlInAppMessageActionListener\n\n    open val inAppMessageViewWrapperFactory: IInAppMessageViewWrapperFactory\n        get() = customInAppMessageViewWrapperFactory ?: defaultInAppMessageViewWrapperFactory\n    open val inAppMessageAnimationFactory: IInAppMessageAnimationFactory\n        get() = customInAppMessageAnimationFactory ?: inAppMessageAnimationFactoryField\n\n    @get:RestrictTo(RestrictTo.Scope.TESTS)\n    open val isActivitySet: Boolean\n        get() = activity != null\n\n    /**\n     * Gets the default [IInAppMessageViewFactory] as returned by the [BrazeInAppMessageManager]\n     * for the given [IInAppMessage].\n     *\n     * @return The [IInAppMessageViewFactory] or null if the message type does not have a [IInAppMessageViewFactory].\n     */\n    open fun getDefaultInAppMessageViewFactory(inAppMessage: IInAppMessage): IInAppMessageViewFactory? {\n        return when (inAppMessage.messageType) {\n            MessageType.SLIDEUP -> inAppMessageSlideupViewFactory\n            MessageType.MODAL -> inAppMessageModalViewFactory\n            MessageType.FULL -> inAppMessageFullViewFactory\n            MessageType.HTML_FULL -> inAppMessageHtmlFullViewFactory\n            MessageType.HTML -> inAppMessageHtmlViewFactory\n            else -> {\n                brazelog(W) {\n                    \"Failed to find view factory for in-app message with type: ${inAppMessage.messageType}\"\n                }\n                null\n            }\n        }\n    }\n\n    open fun getInAppMessageViewFactory(inAppMessage: IInAppMessage): IInAppMessageViewFactory? =\n        customInAppMessageViewFactory ?: getDefaultInAppMessageViewFactory(inAppMessage)\n\n    /**\n     * Sets whether the hardware back button dismisses in-app messages. Defaults to true.\n     * Note that the hardware back button default behavior will be used instead (i.e. the host [Activity]'s\n     * [Activity.onKeyDown] method will be called).\n     */\n    open fun setBackButtonDismissesInAppMessageView(backButtonDismissesInAppMessageView: Boolean) {\n        brazelog { \"In-App Message back button dismissal set to $backButtonDismissesInAppMessageView\" }\n        doesBackButtonDismissInAppMessageViewField = backButtonDismissesInAppMessageView\n    }\n\n    /**\n     * Sets whether the tapping outside the modal in-app message content dismiss the\n     * message. Defaults to false.\n     */\n    open fun setClickOutsideModalViewDismissInAppMessageView(doesDismiss: Boolean) {\n        brazelog { \"Modal In-App Message outside tap dismissal set to $doesDismiss\" }\n        doesClickOutsideModalViewDismissInAppMessageViewField = doesDismiss\n    }\n\n    /**\n     * Assigns a custom [IInAppMessageManagerListener] that will be used when displaying in-app messages. To revert\n     * back to the default [IInAppMessageManagerListener], call this method with null.\n     *\n     *\n     * see [IInAppMessage.isControl]\n     *\n     * @param inAppMessageManagerListener A custom [IInAppMessageManagerListener] or null (to revert back to the\n     * default [IInAppMessageManagerListener]).\n     */\n    open fun setCustomInAppMessageManagerListener(inAppMessageManagerListener: IInAppMessageManagerListener?) {\n        brazelog { \"Custom InAppMessageManagerListener set\" }\n        customInAppMessageManagerListener = inAppMessageManagerListener\n    }\n\n    /**\n     * Assigns a custom [IInAppMessageManagerListener] that will be used when displaying control in-app messages. To revert\n     * back to the default [IInAppMessageManagerListener], call this method with null.\n     *\n     * @param inAppMessageManagerListener A custom [IInAppMessageManagerListener] for control in-app messages or null (to revert back to the\n     * default [IInAppMessageManagerListener]).\n     */\n    open fun setCustomControlInAppMessageManagerListener(inAppMessageManagerListener: IInAppMessageManagerListener?) {\n        brazelog {\n            \"Custom ControlInAppMessageManagerListener set. This listener will only be used for control in-app messages.\"\n        }\n        customControlInAppMessageManagerListener = inAppMessageManagerListener\n    }\n\n    /**\n     * Assigns a custom IHtmlInAppMessageActionListener that will be used during the display of Html in-app messages.\n     *\n     * @param htmlInAppMessageActionListener A custom IHtmlInAppMessageActionListener or null (to revert back to the\n     * default IHtmlInAppMessageActionListener).\n     */\n    open fun setCustomHtmlInAppMessageActionListener(htmlInAppMessageActionListener: IHtmlInAppMessageActionListener?) {\n        brazelog { \"Custom htmlInAppMessageActionListener set\" }\n        customHtmlInAppMessageActionListener = htmlInAppMessageActionListener\n    }\n\n    /**\n     * Assigns a custom IInAppMessageAnimationFactory that will be used to animate the in-app message View. To revert\n     * back to the default IInAppMessageAnimationFactory, call the setCustomInAppMessageAnimationFactory method with null.\n     *\n     * @param inAppMessageAnimationFactory A custom IInAppMessageAnimationFactory or null (to revert back to the default\n     * IInAppMessageAnimationFactory).\n     */\n    open fun setCustomInAppMessageAnimationFactory(inAppMessageAnimationFactory: IInAppMessageAnimationFactory?) {\n        brazelog { \"Custom InAppMessageAnimationFactory set\" }\n        customInAppMessageAnimationFactory = inAppMessageAnimationFactory\n    }\n\n    /**\n     * Assigns a custom IInAppMessageViewFactory that will be used to create the in-app message View. To revert\n     * back to the default IInAppMessageViewFactory, call the setCustomInAppMessageViewFactory method with null.\n     *\n     * @param inAppMessageViewFactory A custom IInAppMessageViewFactory or null (to revert back to the default\n     * IInAppMessageViewFactory).\n     */\n    open fun setCustomInAppMessageViewFactory(inAppMessageViewFactory: IInAppMessageViewFactory?) {\n        brazelog { \"Custom InAppMessageViewFactory set\" }\n        customInAppMessageViewFactory = inAppMessageViewFactory\n    }\n\n    /**\n     * Sets a custom [IInAppMessageViewWrapperFactory] that will be used to\n     * display an [IInAppMessage] to the user.\n     */\n    open fun setCustomInAppMessageViewWrapperFactory(inAppMessageViewWrapperFactory: IInAppMessageViewWrapperFactory?) {\n        brazelog { \"Custom IInAppMessageViewWrapperFactory set\" }\n        customInAppMessageViewWrapperFactory = inAppMessageViewWrapperFactory\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/InAppMessageOperation.kt",
    "content": "package com.braze.ui.inappmessage\n\nimport androidx.annotation.Keep\nimport java.util.*\n\nenum class InAppMessageOperation {\n    DISPLAY_NOW, DISPLAY_LATER, DISCARD;\n\n    @Keep\n    companion object {\n        @JvmStatic\n        fun fromValue(value: String?): InAppMessageOperation? =\n            values().firstOrNull { it.name == value?.uppercase(Locale.US) }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/config/BrazeInAppMessageParams.kt",
    "content": "package com.braze.ui.inappmessage.config\n\nobject BrazeInAppMessageParams {\n    const val MODALIZED_IMAGE_RADIUS_DP = 9.0\n    const val GRAPHIC_MODAL_MAX_WIDTH_DP = 290.0\n    const val GRAPHIC_MODAL_MAX_HEIGHT_DP = 290.0\n    @JvmStatic var modalizedImageRadiusDp = MODALIZED_IMAGE_RADIUS_DP\n    @JvmStatic var graphicModalMaxWidthDp = GRAPHIC_MODAL_MAX_WIDTH_DP\n    @JvmStatic var graphicModalMaxHeightDp = GRAPHIC_MODAL_MAX_HEIGHT_DP\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageAnimationFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.content.res.Resources\nimport android.view.animation.AlphaAnimation\nimport android.view.animation.Animation\nimport com.braze.enums.inappmessage.SlideFrom\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageSlideup\nimport com.braze.ui.inappmessage.IInAppMessageAnimationFactory\nimport com.braze.ui.support.createVerticalAnimation\nimport com.braze.ui.support.setAnimationParams\n\nopen class DefaultInAppMessageAnimationFactory : IInAppMessageAnimationFactory {\n    private val shortAnimationDurationMs =\n        Resources.getSystem().getInteger(android.R.integer.config_shortAnimTime).toLong()\n\n    override fun getOpeningAnimation(inAppMessage: IInAppMessage): Animation? {\n        return if (inAppMessage is InAppMessageSlideup) {\n            if (inAppMessage.slideFrom === SlideFrom.TOP) {\n                createVerticalAnimation(-1f, 0f, shortAnimationDurationMs, false)\n            } else {\n                createVerticalAnimation(1f, 0f, shortAnimationDurationMs, false)\n            }\n        } else {\n            setAnimationParams(AlphaAnimation(0f, 1f), shortAnimationDurationMs, true)\n        }\n    }\n\n    override fun getClosingAnimation(inAppMessage: IInAppMessage): Animation? {\n        return if (inAppMessage is InAppMessageSlideup) {\n            if (inAppMessage.slideFrom === SlideFrom.TOP) {\n                createVerticalAnimation(0f, -1f, shortAnimationDurationMs, false)\n            } else {\n                createVerticalAnimation(0f, 1f, shortAnimationDurationMs, false)\n            }\n        } else {\n            setAnimationParams(AlphaAnimation(1f, 0f), shortAnimationDurationMs, false)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageFullViewFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.view.View\nimport android.view.ViewGroup.MarginLayoutParams\nimport android.widget.RelativeLayout\nimport com.braze.ui.R\nimport com.braze.Braze\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.enums.inappmessage.ImageStyle\nimport com.braze.enums.inappmessage.Orientation\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageFull\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory\nimport com.braze.ui.inappmessage.views.InAppMessageBaseView\nimport com.braze.ui.inappmessage.views.InAppMessageFullView\nimport com.braze.ui.inappmessage.views.InAppMessageImageView\nimport com.braze.ui.support.convertDpToPixels\nimport com.braze.ui.support.isRunningOnTablet\nimport com.braze.ui.support.setHeightOnViewLayoutParams\nimport kotlin.math.min\n\nopen class DefaultInAppMessageFullViewFactory : IInAppMessageViewFactory {\n    @Suppress(\"LongMethod\")\n    override fun createInAppMessageView(\n        activity: Activity,\n        inAppMessage: IInAppMessage\n    ): InAppMessageFullView {\n        val applicationContext = activity.applicationContext\n        val inAppMessageFull = inAppMessage as InAppMessageFull\n        val isGraphic = inAppMessageFull.imageStyle == ImageStyle.GRAPHIC\n        val view = getAppropriateFullView(activity, isGraphic)\n        view.createAppropriateViews(activity, inAppMessageFull, isGraphic)\n\n        // Since this image is the width of the screen, the view bounds are uncapped\n        val imageUrl = InAppMessageBaseView.getAppropriateImageUrl(inAppMessageFull)\n        if (!imageUrl.isNullOrEmpty()) {\n            val brazeImageLoader = Braze.getInstance(applicationContext).imageLoader\n            view.messageImageView?.let {\n                brazeImageLoader.renderUrlIntoInAppMessageView(\n                    applicationContext,\n                    inAppMessage,\n                    imageUrl,\n                    it,\n                    BrazeViewBounds.NO_BOUNDS\n                )\n            }\n        }\n\n        // modal frame should not be clickable.\n        view.frameView?.setOnClickListener(null)\n        view.setMessageBackgroundColor(inAppMessageFull.backgroundColor)\n        inAppMessageFull.frameColor?.let { view.setFrameColor(it) }\n        view.setMessageButtons(inAppMessageFull.messageButtons)\n        view.setMessageCloseButtonColor(inAppMessageFull.closeButtonColor)\n        if (!isGraphic) {\n            inAppMessageFull.message?.let { view.setMessage(it) }\n            view.setMessageTextColor(inAppMessageFull.messageTextColor)\n            inAppMessageFull.header?.let { view.setMessageHeaderText(it) }\n            view.setMessageHeaderTextColor(inAppMessageFull.headerTextColor)\n            view.setMessageHeaderTextAlignment(inAppMessageFull.headerTextAlign)\n            view.setMessageTextAlign(inAppMessageFull.messageTextAlign)\n            view.resetMessageMargins(inAppMessageFull.imageDownloadSuccessful)\n\n            // Only non-graphic full in-app messages should be capped to half the parent height\n            (view.messageImageView as InAppMessageImageView).setToHalfParentHeight(true)\n        }\n        view.setLargerCloseButtonClickArea(view.messageCloseButtonView)\n        resetLayoutParamsIfAppropriate(activity, inAppMessageFull, view)\n        view.setupDirectionalNavigation(inAppMessageFull.messageButtons.size)\n\n        // Get the scrollView, if it exists. For graphic full, it will not\n        val scrollView = view.findViewById<View>(R.id.com_braze_inappmessage_full_scrollview)\n        if (scrollView != null) {\n            val allContentParent = view.findViewById<View>(R.id.com_braze_inappmessage_full_all_content_parent)\n            scrollView.post {\n                // Get the parent height\n                val parentHeight = allContentParent.height\n\n                // Half of that is the Image\n                // So we have another half allotted for us + some margins + the buttons\n                val halfHeight = parentHeight / 2\n\n                // Compute the rest of the height for the ScrollView + buttons + margins\n                val contentView = view.findViewById<View>(R.id.com_braze_inappmessage_full_text_and_button_content_parent)\n                val layoutParams = contentView.layoutParams as MarginLayoutParams\n                var nonScrollViewHeight = layoutParams.bottomMargin + layoutParams.topMargin\n                if (inAppMessageFull.messageButtons.isNotEmpty()) {\n                    // Account for all appropriate height / margins\n                    nonScrollViewHeight += convertDpToPixels(\n                        applicationContext,\n                        BUTTONS_PRESENT_SCROLLVIEW_EXCESS_HEIGHT_VALUE_IN_DP.toDouble()\n                    ).toInt()\n                }\n\n                // The remaining height is the MOST that the scrollView can take up\n                val scrollViewAppropriateHeight =\n                    min(scrollView.height, halfHeight - nonScrollViewHeight)\n\n                // Now set that height for the ScrollView\n                setHeightOnViewLayoutParams(scrollView, scrollViewAppropriateHeight)\n\n                // Request another layout since we changed bounds for everything\n                scrollView.requestLayout()\n                view.messageImageView?.requestLayout()\n            }\n        }\n        return view\n    }\n\n    /**\n     * For in-app messages that have a preferred orientation and are being displayed on tablet,\n     * ensure the in-app message appears in the style of the preferred orientation regardless of\n     * actual screen orientation.\n     *\n     * @param activity\n     * @param inAppMessage\n     * @param view\n     * @return true if params were reset\n     */\n    private fun resetLayoutParamsIfAppropriate(\n        activity: Activity,\n        inAppMessage: IInAppMessage,\n        view: InAppMessageFullView\n    ): Boolean {\n        if (!activity.isRunningOnTablet()) {\n            return false\n        }\n        if (inAppMessage.orientation === Orientation.ANY) {\n            return false\n        }\n        val longEdge = view.longEdge\n        val shortEdge = view.shortEdge\n        if (longEdge > 0 && shortEdge > 0) {\n            val layoutParams: RelativeLayout.LayoutParams = if (inAppMessage.orientation === Orientation.LANDSCAPE) {\n                RelativeLayout.LayoutParams(longEdge, shortEdge)\n            } else {\n                RelativeLayout.LayoutParams(shortEdge, longEdge)\n            }\n            layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)\n            view.messageBackgroundObject?.layoutParams = layoutParams\n            return true\n        }\n        return false\n    }\n\n    @SuppressLint(\"InflateParams\")\n    fun getAppropriateFullView(activity: Activity, isGraphic: Boolean): InAppMessageFullView {\n        return if (isGraphic) {\n            activity.layoutInflater.inflate(\n                R.layout.com_braze_inappmessage_full_graphic,\n                null\n            ) as InAppMessageFullView\n        } else {\n            activity.layoutInflater.inflate(\n                R.layout.com_braze_inappmessage_full,\n                null\n            ) as InAppMessageFullView\n        }\n    }\n\n    companion object {\n        /**\n         * 20dp margin between button / bottom of scrollview.\n         * 44dp height for buttons.\n         */\n        private const val BUTTONS_PRESENT_SCROLLVIEW_EXCESS_HEIGHT_VALUE_IN_DP = 64\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageHtmlFullViewFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport com.braze.ui.R\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageHtmlFull\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory\nimport com.braze.ui.inappmessage.jsinterface.InAppMessageJavascriptInterface\nimport com.braze.ui.inappmessage.listeners.IInAppMessageWebViewClientListener\nimport com.braze.ui.inappmessage.utils.InAppMessageWebViewClient\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlBaseView\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlFullView\nimport com.braze.ui.support.isDeviceNotInTouchMode\n\nopen class DefaultInAppMessageHtmlFullViewFactory(private val inAppMessageWebViewClientListener: IInAppMessageWebViewClientListener) :\n    IInAppMessageViewFactory {\n    @SuppressLint(\"AddJavascriptInterface\")\n    override fun createInAppMessageView(\n        activity: Activity,\n        inAppMessage: IInAppMessage\n    ): InAppMessageHtmlFullView? {\n        val view = activity.layoutInflater\n            .inflate(R.layout.com_braze_inappmessage_html_full, null) as InAppMessageHtmlFullView\n        val config = BrazeConfigurationProvider(activity.applicationContext)\n        if (config.isTouchModeRequiredForHtmlInAppMessages && isDeviceNotInTouchMode(view)) {\n            brazelog(W) {\n                \"The device is not currently in touch mode. \" +\n                    \"This message requires user touch interaction to display properly. Please set \" +\n                    \"setIsTouchModeRequiredForHtmlInAppMessages to false to change this behavior.\"\n            }\n            return null\n        }\n        val context = activity.applicationContext\n        val inAppMessageHtmlFull = inAppMessage as InAppMessageHtmlFull\n        val javascriptInterface = InAppMessageJavascriptInterface(context, inAppMessageHtmlFull)\n        view.setWebViewContent(\n            inAppMessage.message,\n            inAppMessageHtmlFull.localAssetsDirectoryUrl\n        )\n        view.setInAppMessageWebViewClient(\n            InAppMessageWebViewClient(\n                context,\n                inAppMessage,\n                inAppMessageWebViewClientListener\n            )\n        )\n        view.messageWebView?.addJavascriptInterface(\n            javascriptInterface,\n            InAppMessageHtmlBaseView.BRAZE_BRIDGE_PREFIX\n        )\n        return view\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageHtmlViewFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport com.braze.ui.R\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageHtml\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory\nimport com.braze.ui.inappmessage.jsinterface.InAppMessageJavascriptInterface\nimport com.braze.ui.inappmessage.listeners.IInAppMessageWebViewClientListener\nimport com.braze.ui.inappmessage.utils.InAppMessageWebViewClient\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlBaseView\nimport com.braze.ui.inappmessage.views.InAppMessageHtmlView\nimport com.braze.ui.support.isDeviceNotInTouchMode\n\n/**\n * An [IInAppMessageViewFactory] for [InAppMessageHtml] messages.\n */\nopen class DefaultInAppMessageHtmlViewFactory(private val inAppMessageWebViewClientListener: IInAppMessageWebViewClientListener) :\n    IInAppMessageViewFactory {\n    @SuppressLint(\"AddJavascriptInterface\")\n    override fun createInAppMessageView(\n        activity: Activity,\n        inAppMessage: IInAppMessage\n    ): InAppMessageHtmlView? {\n        val context = activity.applicationContext\n        val view = activity.layoutInflater\n            .inflate(R.layout.com_braze_inappmessage_html, null) as InAppMessageHtmlView\n        val config = BrazeConfigurationProvider(context)\n        if (config.isTouchModeRequiredForHtmlInAppMessages && isDeviceNotInTouchMode(view)) {\n            brazelog(W) {\n                \"The device is not currently in touch mode. \" +\n                    \"This message requires user touch interaction to display properly. Please set \" +\n                    \"setIsTouchModeRequiredForHtmlInAppMessages to false to change this behavior.\"\n            }\n            return null\n        }\n        val inAppMessageHtml = inAppMessage as InAppMessageHtml\n        val javascriptInterface = InAppMessageJavascriptInterface(context, inAppMessageHtml)\n        view.setWebViewContent(inAppMessageHtml.message)\n        view.setInAppMessageWebViewClient(\n            InAppMessageWebViewClient(\n                activity.applicationContext,\n                inAppMessageHtml,\n                inAppMessageWebViewClientListener\n            )\n        )\n        view.messageWebView?.addJavascriptInterface(\n            javascriptInterface,\n            InAppMessageHtmlBaseView.BRAZE_BRIDGE_PREFIX\n        )\n        return view\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageModalViewFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport com.braze.ui.R\nimport com.braze.Braze\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.enums.inappmessage.ImageStyle\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageModal\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory\nimport com.braze.ui.inappmessage.views.InAppMessageBaseView\nimport com.braze.ui.inappmessage.views.InAppMessageImageView\nimport com.braze.ui.inappmessage.views.InAppMessageModalView\n\nopen class DefaultInAppMessageModalViewFactory : IInAppMessageViewFactory {\n    override fun createInAppMessageView(\n        activity: Activity,\n        inAppMessage: IInAppMessage\n    ): InAppMessageModalView {\n        val applicationContext = activity.applicationContext\n        val inAppMessageModal = inAppMessage as InAppMessageModal\n        val isGraphic = inAppMessageModal.imageStyle == ImageStyle.GRAPHIC\n        val view = getAppropriateModalView(activity, isGraphic)\n        view.applyInAppMessageParameters(applicationContext, inAppMessageModal)\n        val imageUrl = InAppMessageBaseView.getAppropriateImageUrl(inAppMessageModal)\n        if (!imageUrl.isNullOrEmpty()) {\n            val brazeImageLoader = Braze.getInstance(applicationContext).imageLoader\n            view.messageImageView?.let {\n                brazeImageLoader.renderUrlIntoInAppMessageView(\n                    applicationContext,\n                    inAppMessage,\n                    imageUrl,\n                    it,\n                    BrazeViewBounds.IN_APP_MESSAGE_MODAL\n                )\n            }\n        }\n\n        // Modal frame should only dismiss the message when configured.\n        view.frameView?.setOnClickListener {\n            if (BrazeInAppMessageManager.getInstance().doesClickOutsideModalViewDismissInAppMessageView) {\n                brazelog(I) { \"Dismissing modal after frame click\" }\n                BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(true)\n            }\n        }\n        view.setMessageBackgroundColor(inAppMessage.backgroundColor)\n        inAppMessageModal.frameColor?.let { view.setFrameColor(it) }\n        view.setMessageButtons(inAppMessageModal.messageButtons)\n        view.setMessageCloseButtonColor(inAppMessageModal.closeButtonColor)\n        if (!isGraphic) {\n            inAppMessage.message?.let { view.setMessage(it) }\n            view.setMessageTextColor(inAppMessage.messageTextColor)\n            inAppMessageModal.header?.let { view.setMessageHeaderText(it) }\n            view.setMessageHeaderTextColor(inAppMessageModal.headerTextColor)\n            inAppMessage.icon?.let {\n                view.setMessageIcon(\n                    it,\n                    inAppMessage.iconColor,\n                    inAppMessage.iconBackgroundColor\n                )\n            }\n            view.setMessageHeaderTextAlignment(inAppMessageModal.headerTextAlign)\n            view.setMessageTextAlign(inAppMessageModal.messageTextAlign)\n            view.resetMessageMargins(inAppMessageModal.imageDownloadSuccessful)\n            (view.messageImageView as InAppMessageImageView).setAspectRatio(NON_GRAPHIC_ASPECT_RATIO)\n        }\n        view.setLargerCloseButtonClickArea(view.messageCloseButtonView)\n        view.setupDirectionalNavigation(inAppMessageModal.messageButtons.size)\n        return view\n    }\n\n    @SuppressLint(\"InflateParams\")\n    private fun getAppropriateModalView(\n        activity: Activity,\n        isGraphic: Boolean\n    ): InAppMessageModalView {\n        return if (isGraphic) {\n            activity.layoutInflater.inflate(\n                R.layout.com_braze_inappmessage_modal_graphic,\n                null\n            ) as InAppMessageModalView\n        } else {\n            activity.layoutInflater.inflate(\n                R.layout.com_braze_inappmessage_modal,\n                null\n            ) as InAppMessageModalView\n        }\n    }\n\n    companion object {\n        private const val NON_GRAPHIC_ASPECT_RATIO = 290f / 100f\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageSlideupViewFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.app.Activity\nimport com.braze.ui.R\nimport com.braze.Braze\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageSlideup\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory\nimport com.braze.ui.inappmessage.views.InAppMessageBaseView\nimport com.braze.ui.inappmessage.views.InAppMessageSlideupView\nimport com.braze.ui.support.isDeviceNotInTouchMode\n\nopen class DefaultInAppMessageSlideupViewFactory : IInAppMessageViewFactory {\n    override fun createInAppMessageView(\n        activity: Activity,\n        inAppMessage: IInAppMessage\n    ): InAppMessageSlideupView? {\n        val view = activity.layoutInflater.inflate(\n            R.layout.com_braze_inappmessage_slideup,\n            null\n        ) as InAppMessageSlideupView\n        if (isDeviceNotInTouchMode(view)) {\n            brazelog(W) { \"The device is not currently in touch mode. This message requires user touch interaction to display properly.\" }\n            return null\n        }\n        val inAppMessageSlideup = inAppMessage as InAppMessageSlideup\n        val applicationContext = activity.applicationContext\n        view.applyInAppMessageParameters(inAppMessage)\n        val imageUrl = InAppMessageBaseView.getAppropriateImageUrl(inAppMessageSlideup)\n        if (!imageUrl.isNullOrEmpty()) {\n            val brazeImageLoader = Braze.getInstance(applicationContext).imageLoader\n            view.messageImageView?.let {\n                brazeImageLoader.renderUrlIntoInAppMessageView(\n                    applicationContext,\n                    inAppMessage,\n                    imageUrl,\n                    it,\n                    BrazeViewBounds.IN_APP_MESSAGE_SLIDEUP\n                )\n            }\n        }\n        view.setMessageBackgroundColor(inAppMessageSlideup.backgroundColor)\n        inAppMessageSlideup.message?.let { view.setMessage(it) }\n        view.setMessageTextColor(inAppMessageSlideup.messageTextColor)\n        view.setMessageTextAlign(inAppMessageSlideup.messageTextAlign)\n        inAppMessageSlideup.icon?.let {\n            view.setMessageIcon(\n                it,\n                inAppMessageSlideup.iconColor,\n                inAppMessageSlideup.iconBackgroundColor\n            )\n        }\n        view.setMessageChevron(inAppMessageSlideup.chevronColor, inAppMessageSlideup.clickAction)\n        view.resetMessageMargins(inAppMessageSlideup.imageDownloadSuccessful)\n        return view\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/factories/DefaultInAppMessageViewWrapperFactory.kt",
    "content": "package com.braze.ui.inappmessage.factories\n\nimport android.view.View\nimport android.view.animation.Animation\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.ui.inappmessage.DefaultInAppMessageViewWrapper\nimport com.braze.ui.inappmessage.IInAppMessageViewWrapper\nimport com.braze.ui.inappmessage.IInAppMessageViewWrapperFactory\nimport com.braze.ui.inappmessage.listeners.IInAppMessageViewLifecycleListener\n\n/**\n * The default [IInAppMessageViewWrapperFactory] that returns\n * an instance of [DefaultInAppMessageViewWrapper].\n */\nopen class DefaultInAppMessageViewWrapperFactory : IInAppMessageViewWrapperFactory {\n    override fun createInAppMessageViewWrapper(\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener,\n        configurationProvider: BrazeConfigurationProvider,\n        openingAnimation: Animation?,\n        closingAnimation: Animation?,\n        clickableInAppMessageView: View?\n    ): IInAppMessageViewWrapper {\n        return DefaultInAppMessageViewWrapper(\n            inAppMessageView,\n            inAppMessage,\n            inAppMessageViewLifecycleListener,\n            configurationProvider,\n            openingAnimation,\n            closingAnimation,\n            clickableInAppMessageView\n        )\n    }\n\n    override fun createInAppMessageViewWrapper(\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage,\n        inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener,\n        configurationProvider: BrazeConfigurationProvider,\n        openingAnimation: Animation?,\n        closingAnimation: Animation?,\n        clickableInAppMessageView: View?,\n        buttons: List<View>?,\n        closeButton: View?\n    ): IInAppMessageViewWrapper {\n        return DefaultInAppMessageViewWrapper(\n            inAppMessageView,\n            inAppMessage,\n            inAppMessageViewLifecycleListener,\n            configurationProvider,\n            openingAnimation,\n            closingAnimation,\n            clickableInAppMessageView,\n            buttons,\n            closeButton\n        )\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/jsinterface/InAppMessageJavascriptInterface.kt",
    "content": "package com.braze.ui.inappmessage.jsinterface\n\nimport android.content.Context\nimport android.webkit.JavascriptInterface\nimport androidx.annotation.VisibleForTesting\nimport com.braze.Braze\nimport com.braze.models.inappmessage.IInAppMessageHtml\nimport com.braze.models.outgoing.BrazeProperties\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.requestPushPermissionPrompt\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport org.json.JSONObject\nimport java.math.BigDecimal\n\n/**\n * Used to generate the javascript API in html in-app messages.\n */\nclass InAppMessageJavascriptInterface(\n    private val context: Context,\n    private val inAppMessage: IInAppMessageHtml\n) {\n    @get:JavascriptInterface\n    val user: InAppMessageUserJavascriptInterface = InAppMessageUserJavascriptInterface(context)\n\n    @JavascriptInterface\n    fun changeUser(userId: String, sdkAuthSignature: String?) {\n        Braze.getInstance(context).changeUser(userId, sdkAuthSignature)\n    }\n\n    @JavascriptInterface\n    fun requestImmediateDataFlush() {\n        Braze.getInstance(context).requestImmediateDataFlush()\n    }\n\n    @JavascriptInterface\n    fun logCustomEventWithJSON(eventName: String?, propertiesJSON: String?) {\n        val brazeProperties = parseProperties(propertiesJSON)\n        Braze.getInstance(context).logCustomEvent(eventName, brazeProperties)\n    }\n\n    @JavascriptInterface\n    fun logPurchaseWithJSON(\n        productId: String?,\n        price: Double,\n        currencyCode: String?,\n        quantity: Int,\n        propertiesJSON: String?\n    ) {\n        val brazeProperties = parseProperties(propertiesJSON)\n        Braze.getInstance(context).logPurchase(\n            productId,\n            currencyCode,\n            BigDecimal(price.toString()),\n            quantity,\n            brazeProperties\n        )\n    }\n\n    @JavascriptInterface\n    fun logButtonClick(buttonId: String?) {\n        buttonId?.let { inAppMessage.logButtonClick(it) }\n    }\n\n    @JavascriptInterface\n    fun logClick() {\n        inAppMessage.logClick()\n    }\n\n    @JavascriptInterface\n    fun requestPushPermission() {\n        BrazeInAppMessageManager.getInstance().shouldNextUnregisterBeSkipped = true\n        BrazeInAppMessageManager.getInstance().activity.requestPushPermissionPrompt()\n    }\n\n    @VisibleForTesting\n    fun parseProperties(propertiesJSON: String?): BrazeProperties? {\n        try {\n            if (propertiesJSON != null && propertiesJSON != \"undefined\"\n                && propertiesJSON != \"null\"\n            ) {\n                return BrazeProperties(JSONObject(propertiesJSON))\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to parse properties JSON String: $propertiesJSON\" }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/jsinterface/InAppMessageUserJavascriptInterface.kt",
    "content": "package com.braze.ui.inappmessage.jsinterface\n\nimport android.content.Context\nimport android.webkit.JavascriptInterface\nimport androidx.annotation.VisibleForTesting\nimport com.braze.enums.Gender\nimport com.braze.enums.Month\nimport com.braze.enums.Month.Companion.getMonth\nimport com.braze.enums.NotificationSubscriptionType\nimport com.braze.enums.NotificationSubscriptionType.Companion.fromValue\nimport com.braze.Braze\nimport com.braze.BrazeUser\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.*\n\n@Suppress(\"TooManyFunctions\")\nclass InAppMessageUserJavascriptInterface(private val context: Context) {\n    @JavascriptInterface\n    fun setFirstName(firstName: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setFirstName(firstName)\n        }\n    }\n\n    @JavascriptInterface\n    fun setLastName(lastName: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setLastName(lastName)\n        }\n    }\n\n    @JavascriptInterface\n    fun setEmail(email: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setEmail(email)\n        }\n    }\n\n    @JavascriptInterface\n    fun setGender(genderString: String) {\n        val gender = parseGender(genderString)\n        if (gender == null) {\n            brazelog(W) {\n                \"Failed to parse gender in Braze HTML in-app message \" +\n                    \"javascript interface with gender: $genderString\"\n            }\n        } else {\n            Braze.getInstance(context).runOnUser {\n                it.setGender(gender)\n            }\n        }\n    }\n\n    @JavascriptInterface\n    fun setDateOfBirth(year: Int, monthInt: Int, day: Int) {\n        val month = monthFromInt(monthInt)\n        if (month == null) {\n            brazelog(W) { \"Failed to parse month for value $monthInt\" }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setDateOfBirth(year, month, day)\n        }\n    }\n\n    @JavascriptInterface\n    fun setCountry(country: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setCountry(country)\n        }\n    }\n\n    @JavascriptInterface\n    fun setLanguage(language: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setLanguage(language)\n        }\n    }\n\n    @JavascriptInterface\n    fun setHomeCity(homeCity: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setHomeCity(homeCity)\n        }\n    }\n\n    @JavascriptInterface\n    fun setEmailNotificationSubscriptionType(subscriptionType: String) {\n        val subscriptionTypeEnum = subscriptionTypeFromJavascriptString(subscriptionType)\n        if (subscriptionTypeEnum == null) {\n            brazelog(W) {\n                \"Failed to parse email subscription type in Braze HTML in-app message \" +\n                    \"javascript interface with subscription $subscriptionType\"\n            }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setEmailNotificationSubscriptionType(subscriptionTypeEnum)\n        }\n    }\n\n    @JavascriptInterface\n    fun setPushNotificationSubscriptionType(subscriptionType: String) {\n        val subscriptionTypeEnum = subscriptionTypeFromJavascriptString(subscriptionType)\n        if (subscriptionTypeEnum == null) {\n            brazelog(W) {\n                \"Failed to parse push subscription type in Braze HTML in-app message \" +\n                    \"javascript interface with subscription: $subscriptionType\"\n            }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setPushNotificationSubscriptionType(subscriptionTypeEnum)\n        }\n    }\n\n    @JavascriptInterface\n    fun setPhoneNumber(phoneNumber: String?) {\n        Braze.getInstance(context).runOnUser {\n            it.setPhoneNumber(phoneNumber)\n        }\n    }\n\n    @JavascriptInterface\n    fun setCustomUserAttributeJSON(key: String, jsonStringValue: String) {\n        Braze.getInstance(context).runOnUser {\n            setCustomAttribute(it, key, jsonStringValue)\n        }\n    }\n\n    @JavascriptInterface\n    fun setCustomUserAttributeArray(key: String, jsonArrayString: String?) {\n        val arrayValue = parseStringArrayFromJsonString(jsonArrayString)\n        if (arrayValue == null) {\n            brazelog(W) { \"Failed to set custom attribute array for key $key\" }\n            return\n        }\n        Braze.getInstance(context).runOnUser {\n            it.setCustomAttributeArray(key, arrayValue)\n        }\n    }\n\n    @JavascriptInterface\n    fun addToCustomAttributeArray(key: String, value: String) {\n        Braze.getInstance(context).runOnUser {\n            it.addToCustomAttributeArray(key, value)\n        }\n    }\n\n    @JavascriptInterface\n    fun removeFromCustomAttributeArray(key: String, value: String) {\n        Braze.getInstance(context).runOnUser {\n            it.removeFromCustomAttributeArray(key, value)\n        }\n    }\n\n    @JavascriptInterface\n    fun incrementCustomUserAttribute(attribute: String) {\n        Braze.getInstance(context).runOnUser {\n            it.incrementCustomUserAttribute(attribute)\n        }\n    }\n\n    @JavascriptInterface\n    fun setCustomLocationAttribute(attribute: String, latitude: Double, longitude: Double) {\n        Braze.getInstance(context).runOnUser {\n            it.setLocationCustomAttribute(attribute, latitude, longitude)\n        }\n    }\n\n    @JavascriptInterface\n    fun addAlias(alias: String, label: String) {\n        Braze.getInstance(context).runOnUser {\n            it.addAlias(alias, label)\n        }\n    }\n\n    @JavascriptInterface\n    fun addToSubscriptionGroup(subscriptionGroupId: String) {\n        Braze.getInstance(context).runOnUser {\n            it.addToSubscriptionGroup(subscriptionGroupId)\n        }\n    }\n\n    @JavascriptInterface\n    fun removeFromSubscriptionGroup(subscriptionGroupId: String) {\n        Braze.getInstance(context).runOnUser {\n            it.removeFromSubscriptionGroup(subscriptionGroupId)\n        }\n    }\n\n    @VisibleForTesting\n    @Suppress(\"MagicNumber\")\n    fun monthFromInt(monthInt: Int): Month? {\n        return if (monthInt < 1 || monthInt > 12) {\n            null\n        } else getMonth(monthInt - 1)\n    }\n\n    @VisibleForTesting\n    fun subscriptionTypeFromJavascriptString(subscriptionType: String?): NotificationSubscriptionType? =\n        fromValue(subscriptionType)\n\n    @VisibleForTesting\n    fun setCustomAttribute(user: BrazeUser, key: String, jsonStringValue: String) {\n        try {\n            val jsonObject = JSONObject(jsonStringValue)\n            // JSONObject in Android never deals with float values, which\n            // accounts for why that instanceof check is missing below\n            when (val valueObject = jsonObject[JS_BRIDGE_ATTRIBUTE_VALUE]) {\n                is String -> {\n                    user.setCustomUserAttribute(key, valueObject)\n                }\n                is Boolean -> {\n                    user.setCustomUserAttribute(key, valueObject)\n                }\n                is Int -> {\n                    user.setCustomUserAttribute(key, valueObject)\n                }\n                is Double -> {\n                    user.setCustomUserAttribute(key, valueObject)\n                }\n                else -> {\n                    brazelog(W) {\n                        \"Failed to parse custom attribute type for key: $key\" +\n                            \" and json string value: $jsonStringValue\"\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            brazelog(E, e) {\n                \"Failed to parse custom attribute type for key: $key\" +\n                    \" and json string value: $jsonStringValue\"\n            }\n        }\n    }\n\n    @VisibleForTesting\n    fun parseStringArrayFromJsonString(jsonArrayString: String?): Array<String?>? {\n        try {\n            val parsedArray = JSONArray(jsonArrayString)\n            return MutableList<String?>(parsedArray.length()) { i -> parsedArray.getString(i) }.toTypedArray()\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to parse custom attribute array\" }\n        }\n        return null\n    }\n\n    @VisibleForTesting\n    @Suppress(\"ReturnCount\")\n    fun parseGender(genderString: String): Gender? {\n        when (genderString.lowercase(Locale.US)) {\n            Gender.MALE.forJsonPut() -> {\n                return Gender.MALE\n            }\n            Gender.FEMALE.forJsonPut() -> {\n                return Gender.FEMALE\n            }\n            Gender.OTHER.forJsonPut() -> {\n                return Gender.OTHER\n            }\n            Gender.UNKNOWN.forJsonPut() -> {\n                return Gender.UNKNOWN\n            }\n            Gender.NOT_APPLICABLE.forJsonPut() -> {\n                return Gender.NOT_APPLICABLE\n            }\n            Gender.PREFER_NOT_TO_SAY.forJsonPut() -> {\n                return Gender.PREFER_NOT_TO_SAY\n            }\n            else -> return null\n        }\n    }\n\n    companion object {\n        const val JS_BRIDGE_ATTRIBUTE_VALUE = \"value\"\n\n        private fun Braze.runOnUser(block: (user: BrazeUser) -> Unit) {\n            this.getCurrentUser {\n                block(it)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/DefaultHtmlInAppMessageActionListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nopen class DefaultHtmlInAppMessageActionListener : IHtmlInAppMessageActionListener\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/DefaultInAppMessageManagerListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageThemeable\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.InAppMessageOperation\nimport com.braze.ui.support.isDeviceInNightMode\n\n/**\n * Default implementation of [IInAppMessageManagerListener]\n *\n * This only overrides the [beforeInAppMessageDisplayed]. The rest of the functions take the\n * defaults in [IInAppMessageManagerListener]\n */\nopen class DefaultInAppMessageManagerListener : IInAppMessageManagerListener {\n    override fun beforeInAppMessageDisplayed(inAppMessage: IInAppMessage): InAppMessageOperation {\n        if (inAppMessage is IInAppMessageThemeable) {\n            BrazeInAppMessageManager.getInstance().applicationContext?.let { appContext ->\n                if (isDeviceInNightMode(appContext)) {\n                    inAppMessage.enableDarkTheme()\n                }\n            }\n        }\n        return InAppMessageOperation.DISPLAY_NOW\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/DefaultInAppMessageViewLifecycleListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport android.net.Uri\nimport android.view.View\nimport com.braze.enums.Channel\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.enums.inappmessage.ClickAction\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageHtml\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeFunctionNotImplemented\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.WebContentUtils.getHtmlInAppMessageAssetCacheDirectory\nimport com.braze.support.deleteFileOrDirectory\nimport com.braze.support.toBundle\nimport com.braze.ui.BrazeDeeplinkHandler\nimport com.braze.ui.actions.NewsfeedAction\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport kotlinx.coroutines.launch\n\nopen class DefaultInAppMessageViewLifecycleListener : IInAppMessageViewLifecycleListener {\n    private val inAppMessageManager: BrazeInAppMessageManager\n        get() = BrazeInAppMessageManager.getInstance()\n\n    override fun beforeOpened(inAppMessageView: View, inAppMessage: IInAppMessage) {\n        // Note that the client method must be called before any default processing below\n        inAppMessageManager.inAppMessageManagerListener.beforeInAppMessageViewOpened(inAppMessageView, inAppMessage)\n        brazelog { \"IInAppMessageViewLifecycleListener.beforeOpened called.\" }\n        inAppMessage.logImpression()\n    }\n\n    override fun afterOpened(inAppMessageView: View, inAppMessage: IInAppMessage) {\n        brazelog { \"IInAppMessageViewLifecycleListener.afterOpened called.\" }\n\n        // Note that the client method must be called after any default processing above\n        inAppMessageManager.inAppMessageManagerListener.afterInAppMessageViewOpened(inAppMessageView, inAppMessage)\n    }\n\n    override fun beforeClosed(inAppMessageView: View, inAppMessage: IInAppMessage) {\n        // Note that the client method must be called before any default processing below\n        inAppMessageManager.inAppMessageManagerListener.beforeInAppMessageViewClosed(inAppMessageView, inAppMessage)\n        brazelog { \"IInAppMessageViewLifecycleListener.beforeClosed called.\" }\n    }\n\n    override fun afterClosed(inAppMessage: IInAppMessage) {\n        brazelog { \"IInAppMessageViewLifecycleListener.afterClosed called.\" }\n        inAppMessageManager.resetAfterInAppMessageClose()\n        if (inAppMessage is IInAppMessageHtml) {\n            startClearHtmlInAppMessageAssetsThread()\n        }\n        inAppMessage.onAfterClosed()\n\n        // Note that the client method must be called after any default processing above\n        inAppMessageManager.inAppMessageManagerListener.afterInAppMessageViewClosed(inAppMessage)\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onClicked(\n        inAppMessageCloser: com.braze.ui.inappmessage.InAppMessageCloser,\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage\n    ) {\n        brazelog { \"IInAppMessageViewLifecycleListener.onClicked called.\" }\n        inAppMessage.logClick()\n\n        // Perform the in-app message clicked listener action from the host application first. This give\n        // the app the option to override the values that are sent from the server and handle the\n        // in-app message differently depending on where the user is in the app.\n        //\n        // To modify the default in-app message clicked behavior, mutate the necessary in-app message members. As\n        // an example, if the in-app message were to navigate to the news feed when it was clicked, the\n        // behavior can be cancelled by setting the click action to NONE.\n        @Suppress(\"SwallowedException\")\n        val wasHandled = try {\n            val wasHandledLegacy = inAppMessageManager.inAppMessageManagerListener.onInAppMessageClicked(inAppMessage, inAppMessageCloser)\n            brazelog { \"Deprecated onInAppMessageClicked(inAppMessage, inAppMessageCloser) called.\" }\n            wasHandledLegacy\n        } catch (e: BrazeFunctionNotImplemented) {\n            brazelog { \"Using non-deprecated onInAppMessageClicked(inAppMessage)\" }\n            inAppMessageManager.inAppMessageManagerListener.onInAppMessageClicked(inAppMessage)\n        }\n        if (!wasHandled) {\n            // Perform the default (or modified) in-app message clicked behavior.\n            performInAppMessageClicked(inAppMessage, inAppMessageCloser)\n        }\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onButtonClicked(\n        inAppMessageCloser: com.braze.ui.inappmessage.InAppMessageCloser,\n        messageButton: MessageButton,\n        inAppMessageImmersive: IInAppMessageImmersive\n    ) {\n        brazelog { \"IInAppMessageViewLifecycleListener.onButtonClicked called.\" }\n        inAppMessageImmersive.logButtonClick(messageButton)\n        @Suppress(\"SwallowedException\")\n        val wasHandled =\n            try {\n                inAppMessageManager.inAppMessageManagerListener.onInAppMessageButtonClicked(\n                    inAppMessageImmersive,\n                    messageButton,\n                    inAppMessageCloser\n                )\n            } catch (e: BrazeFunctionNotImplemented) {\n                inAppMessageManager.inAppMessageManagerListener.onInAppMessageButtonClicked(\n                    inAppMessageImmersive,\n                    messageButton\n                )\n            }\n        if (!wasHandled) {\n            // Perform the default (or modified) in-app message button clicked behavior.\n            performInAppMessageButtonClicked(messageButton, inAppMessageImmersive, inAppMessageCloser)\n        }\n    }\n\n    override fun onDismissed(inAppMessageView: View, inAppMessage: IInAppMessage) {\n        brazelog { \"IInAppMessageViewLifecycleListener.onDismissed called.\" }\n        inAppMessageManager.inAppMessageManagerListener.onInAppMessageDismissed(inAppMessage)\n    }\n\n    @Suppress(\"DEPRECATION\")\n    private fun performInAppMessageButtonClicked(\n        messageButton: MessageButton,\n        inAppMessage: IInAppMessage,\n        inAppMessageCloser: com.braze.ui.inappmessage.InAppMessageCloser\n    ) {\n        performClickAction(\n            messageButton.clickAction,\n            inAppMessage,\n            inAppMessageCloser,\n            messageButton.uri,\n            messageButton.openUriInWebview\n        )\n    }\n\n    @Suppress(\"DEPRECATION\")\n    private fun performInAppMessageClicked(inAppMessage: IInAppMessage, inAppMessageCloser: com.braze.ui.inappmessage.InAppMessageCloser) {\n        performClickAction(\n            inAppMessage.clickAction,\n            inAppMessage,\n            inAppMessageCloser,\n            inAppMessage.uri,\n            inAppMessage.openUriInWebView\n        )\n    }\n\n    @Suppress(\"DEPRECATION\")\n    private fun performClickAction(\n        clickAction: ClickAction,\n        inAppMessage: IInAppMessage,\n        inAppMessageCloser: com.braze.ui.inappmessage.InAppMessageCloser,\n        clickUri: Uri?,\n        openUriInWebview: Boolean\n    ) {\n        val activity = inAppMessageManager.activity\n        if (activity == null) {\n            brazelog(W) { \"Can't perform click action because the cached activity is null.\" }\n            return\n        }\n        when (clickAction) {\n            ClickAction.NEWS_FEED -> {\n                inAppMessageCloser.close(false)\n                val newsfeedAction = NewsfeedAction(\n                    inAppMessage.extras.toBundle(),\n                    Channel.INAPP_MESSAGE\n                )\n                BrazeDeeplinkHandler.getInstance()\n                    .gotoNewsFeed(activity, newsfeedAction)\n            }\n            ClickAction.URI -> {\n                inAppMessageCloser.close(false)\n                if (clickUri == null) {\n                    brazelog { \"clickUri is null, not performing click action\" }\n                    return\n                }\n                val uriAction = BrazeDeeplinkHandler.getInstance().createUriActionFromUri(\n                    clickUri, inAppMessage.extras.toBundle(),\n                    openUriInWebview, Channel.INAPP_MESSAGE\n                )\n\n                val appContext = inAppMessageManager.applicationContext\n                if (appContext == null) {\n                    brazelog { \"appContext is null, not performing click action\" }\n                    return\n                } else {\n                    BrazeDeeplinkHandler.getInstance().gotoUri(appContext, uriAction)\n                }\n            }\n            ClickAction.NONE -> inAppMessageCloser.close(inAppMessage.animateOut)\n            else -> inAppMessageCloser.close(false)\n        }\n    }\n\n    private fun startClearHtmlInAppMessageAssetsThread() {\n        BrazeCoroutineScope.launch {\n            val inAppMessageActivity = BrazeInAppMessageManager.getInstance().activity\n            if (inAppMessageActivity != null) {\n                val internalStorageCacheDirectory = getHtmlInAppMessageAssetCacheDirectory(inAppMessageActivity)\n                deleteFileOrDirectory(internalStorageCacheDirectory)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/DefaultInAppMessageWebViewClientListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport android.os.Bundle\nimport androidx.annotation.VisibleForTesting\nimport com.braze.enums.Channel\nimport com.braze.Braze.Companion.getInstance\nimport com.braze.enums.inappmessage.MessageType\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageHtml\nimport com.braze.models.outgoing.BrazeProperties\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.isLocalUri\nimport com.braze.support.toBundle\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance as getDeeplinkHandlerInstance\nimport com.braze.ui.actions.NewsfeedAction\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.utils.InAppMessageWebViewClient\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\n\nopen class DefaultInAppMessageWebViewClientListener : IInAppMessageWebViewClientListener {\n    private val inAppMessageManager: BrazeInAppMessageManager\n        get() = BrazeInAppMessageManager.getInstance()\n\n    override fun onCloseAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle) {\n        brazelog { \"IInAppMessageWebViewClientListener.onCloseAction called.\" }\n        logHtmlInAppMessageClick(inAppMessage, queryBundle)\n\n        // Dismiss the in-app message due to the close action\n        inAppMessageManager.hideCurrentlyDisplayingInAppMessage(true)\n        inAppMessageManager.htmlInAppMessageActionListener.onCloseClicked(\n            inAppMessage,\n            url,\n            queryBundle\n        )\n    }\n\n    override fun onNewsfeedAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle) {\n        brazelog { \"IInAppMessageWebViewClientListener.onNewsfeedAction called.\" }\n        if (inAppMessageManager.activity == null) {\n            brazelog(W) { \"Can't perform news feed action because the cached activity is null.\" }\n            return\n        }\n        // Log a click since the user left to the newsfeed\n        logHtmlInAppMessageClick(inAppMessage, queryBundle)\n        val wasHandled = inAppMessageManager.htmlInAppMessageActionListener.onNewsfeedClicked(\n            inAppMessage,\n            url,\n            queryBundle\n        )\n        if (!wasHandled) {\n            inAppMessage.animateOut = false\n            // Dismiss the in-app message since we're navigating away to the news feed\n            inAppMessageManager.hideCurrentlyDisplayingInAppMessage(false)\n            val newsfeedAction = NewsfeedAction(\n                inAppMessage.extras.toBundle(),\n                Channel.INAPP_MESSAGE\n            )\n            inAppMessageManager.activity?.let { activity ->\n                getDeeplinkHandlerInstance().gotoNewsFeed(activity, newsfeedAction)\n            }\n        }\n    }\n\n    override fun onCustomEventAction(\n        inAppMessage: IInAppMessage,\n        url: String,\n        queryBundle: Bundle\n    ) {\n        brazelog { \"IInAppMessageWebViewClientListener.onCustomEventAction called.\" }\n        if (inAppMessageManager.activity == null) {\n            brazelog(W) { \"Can't perform custom event action because the activity is null.\" }\n            return\n        }\n        val wasHandled = inAppMessageManager.htmlInAppMessageActionListener.onCustomEventFired(\n            inAppMessage,\n            url,\n            queryBundle\n        )\n        if (!wasHandled) {\n            val customEventName = parseCustomEventNameFromQueryBundle(queryBundle)\n            if (customEventName.isNullOrBlank()) {\n                return\n            }\n            val customEventProperties = parsePropertiesFromQueryBundle(queryBundle)\n            inAppMessageManager.activity?.let { activity ->\n                getInstance(activity).logCustomEvent(\n                    customEventName,\n                    customEventProperties\n                )\n            }\n        }\n    }\n\n    override fun onOtherUrlAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle) {\n        brazelog { \"IInAppMessageWebViewClientListener.onOtherUrlAction called.\" }\n        if (inAppMessageManager.activity == null) {\n            brazelog(W) { \"Can't perform other url action because the cached activity is null. Url: $url\" }\n            return\n        }\n        // Log a click since the uri link was followed\n        logHtmlInAppMessageClick(inAppMessage, queryBundle)\n        val wasHandled = inAppMessageManager.htmlInAppMessageActionListener.onOtherUrlAction(\n            inAppMessage,\n            url,\n            queryBundle\n        )\n        if (wasHandled) {\n            brazelog(V) {\n                \"HTML message action listener handled url in onOtherUrlAction. Doing nothing further. Url: $url\"\n            }\n            return\n        }\n\n        // Parse the action\n        val useWebViewForWebLinks = parseUseWebViewFromQueryBundle(inAppMessage, queryBundle)\n        val inAppMessageBundle = inAppMessage.extras.toBundle()\n        inAppMessageBundle.putAll(queryBundle)\n        val uriAction = getDeeplinkHandlerInstance().createUriActionFromUrlString(\n            url,\n            inAppMessageBundle,\n            useWebViewForWebLinks,\n            Channel.INAPP_MESSAGE\n        )\n        if (uriAction == null) {\n            brazelog(W) { \"UriAction is null. Not passing any URI to BrazeDeeplinkHandler. Url: $url\" }\n            return\n        }\n\n        // If a local Uri is being handled here, then we want to keep the user in the Html in-app message and not hide the current in-app message.\n        val uri = uriAction.uri\n        if (uri.isLocalUri()) {\n            brazelog(W) {\n                \"Not passing local uri to BrazeDeeplinkHandler. Got local uri: $uri for url: $url\"\n            }\n            return\n        }\n\n        // Handle the action if it's not a local Uri\n        inAppMessage.animateOut = false\n        // Dismiss the in-app message since we're handling the URI outside of the in-app message webView\n        inAppMessageManager.hideCurrentlyDisplayingInAppMessage(false)\n        inAppMessageManager.activity?.let { activity ->\n            getDeeplinkHandlerInstance().gotoUri(activity, uriAction)\n        }\n    }\n\n    companion object {\n        private const val HTML_IN_APP_MESSAGE_CUSTOM_EVENT_NAME_KEY = \"name\"\n\n        @JvmStatic\n        @VisibleForTesting\n        fun parseUseWebViewFromQueryBundle(\n            inAppMessage: IInAppMessage,\n            queryBundle: Bundle\n        ): Boolean {\n            var isAnyQueryFlagSet = false\n            var isDeepLinkFlagSet = false\n            if (queryBundle.containsKey(InAppMessageWebViewClient.QUERY_NAME_DEEPLINK)) {\n                isDeepLinkFlagSet = queryBundle.getString(InAppMessageWebViewClient.QUERY_NAME_DEEPLINK).toBoolean()\n                isAnyQueryFlagSet = true\n            }\n            var isExternalOpenFlagSet = false\n            if (queryBundle.containsKey(InAppMessageWebViewClient.QUERY_NAME_EXTERNAL_OPEN)) {\n                isExternalOpenFlagSet = queryBundle.getString(InAppMessageWebViewClient.QUERY_NAME_EXTERNAL_OPEN).toBoolean()\n                isAnyQueryFlagSet = true\n            }\n            var useWebViewForWebLinks = inAppMessage.openUriInWebView\n            if (isAnyQueryFlagSet) {\n                useWebViewForWebLinks = !(isDeepLinkFlagSet || isExternalOpenFlagSet)\n            }\n            return useWebViewForWebLinks\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        fun logHtmlInAppMessageClick(inAppMessage: IInAppMessage, queryBundle: Bundle) {\n            if (queryBundle.containsKey(InAppMessageWebViewClient.QUERY_NAME_BUTTON_ID)) {\n                val inAppMessageHtml = inAppMessage as IInAppMessageHtml\n                queryBundle.getString(InAppMessageWebViewClient.QUERY_NAME_BUTTON_ID)?.let {\n                    inAppMessageHtml.logButtonClick(it)\n                }\n            } else if (inAppMessage.messageType === MessageType.HTML_FULL) {\n                // HTML Full messages are the only html type that log clicks implicitly\n                inAppMessage.logClick()\n            }\n        }\n\n        @JvmStatic\n        @VisibleForTesting\n        fun parseCustomEventNameFromQueryBundle(queryBundle: Bundle): String? =\n            queryBundle.getString(HTML_IN_APP_MESSAGE_CUSTOM_EVENT_NAME_KEY)\n\n        @JvmStatic\n        @VisibleForTesting\n        fun parsePropertiesFromQueryBundle(queryBundle: Bundle): BrazeProperties {\n            val customEventProperties = BrazeProperties()\n            for (key in queryBundle.keySet()) {\n                if (key != HTML_IN_APP_MESSAGE_CUSTOM_EVENT_NAME_KEY) {\n                    val propertyValue = queryBundle.getString(key, null)\n                    if (!propertyValue.isNullOrBlank()) {\n                        customEventProperties.addProperty(key, propertyValue)\n                    }\n                }\n            }\n            return customEventProperties\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/IHtmlInAppMessageActionListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport android.os.Bundle\nimport com.braze.models.inappmessage.IInAppMessage\n\n/**\n * The [IHtmlInAppMessageActionListener] allows for the overriding of the default Braze display handling\n * and setting custom behavior during the display of HTML In-App Messages.\n */\ninterface IHtmlInAppMessageActionListener {\n    /**\n     * @param inAppMessage the In-App Message that was closed.\n     * @param url the url clicked.\n     * @param queryBundle a bundle of the query part of the url.\n     */\n    fun onCloseClicked(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle) {}\n\n    /**\n     * @param inAppMessage the In-App Message being displayed.\n     * @param url the url clicked.\n     * @param queryBundle a bundle of the query part of the url.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled. If\n     * true, Braze will log a click and do nothing. If false, Braze will also navigate to the Newsfeed.\n     */\n    fun onNewsfeedClicked(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle): Boolean = false\n\n    /**\n     * @param inAppMessage the In-App Message being displayed.\n     * @param url the url clicked.\n     * @param queryBundle a bundle of the query part of the url.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled. If\n     * true, Braze will do nothing. If false, Braze will log the custom event.\n     */\n    fun onCustomEventFired(\n        inAppMessage: IInAppMessage,\n        url: String,\n        queryBundle: Bundle\n    ): Boolean = false\n\n    /**\n     * @param inAppMessage the In-App Message being displayed.\n     * @param url the url clicked.\n     * @param queryBundle a bundle of the query part of the url.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled. If\n     * true, Braze will log a click and do nothing. If false, Braze will also handle the URL.\n     */\n    fun onOtherUrlAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle): Boolean = false\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/IInAppMessageManagerListener.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\npackage com.braze.ui.inappmessage.listeners\n\nimport android.view.View\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeFunctionNotImplemented\nimport com.braze.ui.inappmessage.InAppMessageCloser\nimport com.braze.ui.inappmessage.InAppMessageOperation\n\n/**\n * The IInAppMessageManagerListener returns the in-app message at specific\n * events in its control flow and gives the host app the option of\n * overriding Braze's default display handling and implementing its own custom behavior.\n *\n *\n * If you are implementing Unity, you must use IBrazeUnityInAppMessageListener instead.\n *\n *\n * See [BrazeInAppMessageManager]\n */\ninterface IInAppMessageManagerListener {\n    /**\n     * @param inAppMessage The in-app message that is currently requested for display.\n     * @return InAppMessageOperation indicating how to handle the candidate in-app message.\n     */\n    fun beforeInAppMessageDisplayed(inAppMessage: IInAppMessage): InAppMessageOperation = InAppMessageOperation.DISPLAY_NOW\n\n    /**\n     * @param inAppMessage       The clicked in-app message.\n     * @param inAppMessageCloser Closing should not be animated if transitioning to a new activity.\n     * If remaining in the same activity, closing should be animated.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled.\n     * If true, Braze will only log a click and do nothing else. If false, Braze will\n     * log a click and also close the in-app message automatically.\n     */\n    @Deprecated(\"InAppMessageCloser is deprecated\", ReplaceWith(\"onInAppMessageClicked(inAppMessage)\"))\n    fun onInAppMessageClicked(inAppMessage: IInAppMessage, inAppMessageCloser: InAppMessageCloser?): Boolean = throw BrazeFunctionNotImplemented\n\n    /**\n     * @param inAppMessage       The clicked in-app message.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled.\n     * If true, Braze will only log a click and do nothing else. If false, Braze will\n     * log a click and also close the in-app message automatically.\n     */\n    fun onInAppMessageClicked(inAppMessage: IInAppMessage) = false\n\n    /**\n     * @param inAppMessage       The clicked in-app message.\n     * @param button             The clicked message button.\n     * @param inAppMessageCloser Closing should not be animated if transitioning to a new activity.\n     * If remaining in the same activity, closing should be animated.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled.\n     * If true, Braze will only log a click and do nothing else. If false, Braze will\n     * log a click and also close the in-app message automatically.\n     */\n    @Deprecated(\"InAppMessageCloser is deprecated\", ReplaceWith(\"onInAppMessageButtonClicked(inAppMessage, button)\"))\n    fun onInAppMessageButtonClicked(\n        inAppMessage: IInAppMessage,\n        button: MessageButton,\n        inAppMessageCloser: InAppMessageCloser?\n    ): Boolean = throw BrazeFunctionNotImplemented\n\n    /**\n     * @param inAppMessage       The clicked in-app message.\n     * @param button             The clicked message button.\n     * @return boolean flag to indicate to Braze whether the click has been manually handled.\n     * If true, Braze will only log a click and do nothing else. If false, Braze will\n     * log a click and also close the in-app message automatically.\n     */\n    fun onInAppMessageButtonClicked(inAppMessage: IInAppMessage, button: MessageButton) = false\n\n    /**\n     * @param inAppMessage the in-app message that was closed.\n     */\n    fun onInAppMessageDismissed(inAppMessage: IInAppMessage) {}\n\n    /**\n     * Called before the in-app message View is added to the layout.\n     *\n     *\n     * Note that this is called before any default processing in\n     * [DefaultInAppMessageViewLifecycleListener] takes place.\n     *\n     * @param inAppMessageView The [View] representing the [IInAppMessage].\n     * @param inAppMessage     The [IInAppMessage] being displayed.\n     */\n    fun beforeInAppMessageViewOpened(inAppMessageView: View, inAppMessage: IInAppMessage) {}\n\n    /**\n     * Called after the in-app message View has been added to the layout\n     * (and the appearing animation has completed).\n     *\n     *\n     * Note that this is called after any default processing in\n     * [DefaultInAppMessageViewLifecycleListener] takes place.\n     *\n     * @param inAppMessageView The [View] representing the [IInAppMessage].\n     * @param inAppMessage     The [IInAppMessage] being displayed.\n     */\n    fun afterInAppMessageViewOpened(inAppMessageView: View, inAppMessage: IInAppMessage) {}\n\n    /**\n     * Called before the in-app message View is removed from the layout\n     * (and before any closing animation starts).\n     *\n     *\n     * Note that this is called before any default processing in\n     * [DefaultInAppMessageViewLifecycleListener] takes place.\n     *\n     * @param inAppMessageView The [View] representing the [IInAppMessage].\n     * @param inAppMessage     The [IInAppMessage] being displayed.\n     */\n    fun beforeInAppMessageViewClosed(inAppMessageView: View, inAppMessage: IInAppMessage) {}\n\n    /**\n     * Called after the in-app message View has been removed from the\n     * layout (and the disappearing animation has completed).\n     *\n     *\n     * Note that this is called after any default processing in\n     * [DefaultInAppMessageViewLifecycleListener] takes place.\n     *\n     * @param inAppMessage The [IInAppMessage] being displayed.\n     */\n    fun afterInAppMessageViewClosed(inAppMessage: IInAppMessage) {}\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/IInAppMessageViewLifecycleListener.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage com.braze.ui.inappmessage.listeners\n\nimport android.view.View\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.ui.inappmessage.InAppMessageCloser\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.models.inappmessage.IInAppMessageImmersive\n\n/**\n * [IInAppMessageViewLifecycleListener] returns the in-app message view at specific events\n * in its display lifecycle for potential further processing and modification.\n *\n * For use cases unrelated to view customization, such as suppressing display or performing\n * custom click handling, see [IInAppMessageManagerListener]\n */\ninterface IInAppMessageViewLifecycleListener {\n    /**\n     * Called before the in-app message View is added to the root layout.\n     * @param inAppMessageView\n     * @param inAppMessage\n     */\n    fun beforeOpened(inAppMessageView: View, inAppMessage: IInAppMessage)\n\n    /**\n     * Called after the in-app message View has been added to the root layout\n     * (and the appearing animation has completed).\n     * @param inAppMessageView\n     * @param inAppMessage\n     */\n    fun afterOpened(inAppMessageView: View, inAppMessage: IInAppMessage)\n\n    /**\n     * Called before the in-app message View is removed (and before any closing\n     * animation starts).\n     * @param inAppMessageView\n     * @param inAppMessage\n     */\n    fun beforeClosed(inAppMessageView: View, inAppMessage: IInAppMessage)\n\n    /**\n     * Called after the in-app message View has been removed from the root\n     * layout (and the disappearing animation has completed).\n     * @param inAppMessage\n     */\n    fun afterClosed(inAppMessage: IInAppMessage)\n\n    /**\n     * Called when the in-app message View is clicked.\n     * @param inAppMessageCloser\n     * @param inAppMessageView\n     * @param inAppMessage\n     */\n    fun onClicked(\n        inAppMessageCloser: InAppMessageCloser,\n        inAppMessageView: View,\n        inAppMessage: IInAppMessage\n    )\n\n    /**\n     * Called when an in-app message Button is clicked.\n     * @param inAppMessageCloser\n     * @param messageButton\n     * @param inAppMessageImmersive\n     */\n    fun onButtonClicked(\n        inAppMessageCloser: InAppMessageCloser,\n        messageButton: MessageButton,\n        inAppMessageImmersive: IInAppMessageImmersive\n    )\n\n    /**\n     * Called when the in-app message View is dismissed.\n     * @param inAppMessageView\n     * @param inAppMessage\n     */\n    fun onDismissed(inAppMessageView: View, inAppMessage: IInAppMessage)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/IInAppMessageWebViewClientListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport android.os.Bundle\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.ui.inappmessage.utils.InAppMessageWebViewClient\n\n/**\n * The [IInAppMessageWebViewClientListener] is called at specific events during the display of an Html\n * In-App Message. Button clicks that occur inside an HTML In-App Message are routed to this listener\n * and not the [IInAppMessageViewLifecycleListener]. However, the display lifecycle of the HTML In-App Message is\n * still handled by the [IInAppMessageViewLifecycleListener].\n *\n * See [InAppMessageWebViewClient].\n */\ninterface IInAppMessageWebViewClientListener {\n    /**\n     * Called when a close URL (appboy://close) is followed in an HTML In App Message.\n     *\n     * @param inAppMessage the inAppMessage\n     * @param url          the url that triggered the close\n     * @param queryBundle a bundle of the query part of url\n     */\n    fun onCloseAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle)\n\n    /**\n     * Called when a Newsfeed URL (appboy://newsfeed) is followed in an HTML In App Message.\n     *\n     * @param inAppMessage the inAppMessage\n     * @param url          the url that triggered the action\n     * @param queryBundle a bundle of the query part of url\n     */\n    fun onNewsfeedAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle)\n\n    /**\n     * Called when the window location is set to a Custom Event URL (appboy://customEvent) in an HTML In App Message.\n     *\n     * @param inAppMessage the inAppMessage\n     * @param url          the url that triggered the action\n     * @param queryBundle a bundle of the query part of url\n     */\n    fun onCustomEventAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle)\n\n    /**\n     * Called when a non `appboy` scheme url is encountered.\n     *\n     * @param inAppMessage the inAppMessage\n     * @param url          the url pressed\n     * @param queryBundle a bundle of the query part of url\n     */\n    fun onOtherUrlAction(inAppMessage: IInAppMessage, url: String, queryBundle: Bundle)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/IWebViewClientStateListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nfun interface IWebViewClientStateListener {\n    /**\n     * Fired when [android.webkit.WebViewClient.onPageFinished] has been called.\n     */\n    fun onPageFinished()\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/SwipeDismissTouchListener.java",
    "content": "package com.braze.ui.inappmessage.listeners;\n\n/*\n * Copyright 2013 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.annotation.TargetApi;\nimport android.view.MotionEvent;\nimport android.view.VelocityTracker;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\n\n/**\n * A {@link android.view.View.OnTouchListener} that makes any {@link android.view.View} dismissable when the\n * user swipes (drags her finger) horizontally across the view.\n *\n * <p><em>For {@link android.widget.ListView} list items that don't manage their own touch events\n * (i.e. you're using\n * {@link android.widget.ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)}\n * or an equivalent listener on {@link android.app.ListActivity} or\n * {@link android.app.ListFragment}, use {@link SwipeDismissListViewTouchListener} instead.</em></p>\n *\n * <p>Example usage:</p>\n *\n * <pre>\n * view.setOnTouchListener(new SwipeDismissTouchListener(\n *         view,\n *         null, // Optional token/cookie object\n *         new SwipeDismissTouchListener.OnDismissCallback() {\n *             public void onDismiss(View view, Object token) {\n *                 parent.removeView(view);\n *             }\n *         }));\n * </pre>\n *\n * <p>This class Requires API level 12 or later due to use of {@link\n * android.view.ViewPropertyAnimator}.</p>\n *\n * @see SwipeDismissListViewTouchListener\n */\n@SuppressWarnings(\"checkstyle:missingswitchdefault\")\npublic class SwipeDismissTouchListener implements View.OnTouchListener {\n  // Cached ViewConfiguration and system-wide constant values\n  private final int mSlop;\n  private final int mMinFlingVelocity;\n  private final int mMaxFlingVelocity;\n  private final long mAnimationTime;\n\n  // Fixed properties\n  private final View mView;\n  private final DismissCallbacks mCallbacks;\n  private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero\n\n  // Transient properties\n  private float mDownX;\n  private float mDownY;\n  private boolean mSwiping;\n  private int mSwipingSlop;\n  private final Object mToken;\n  private VelocityTracker mVelocityTracker;\n  private float mTranslationX;\n\n  /**\n   * The callback interface used by {@link SwipeDismissTouchListener} to inform its client\n   * about a successful dismissal of the view for which it was created.\n   */\n  public interface DismissCallbacks {\n    /**\n     * Called to determine whether the view can be dismissed.\n     */\n    boolean canDismiss(Object token);\n\n    /**\n     * Called when the user has indicated they she would like to dismiss the view.\n     *\n     * @param view The originating {@link View} to be dismissed.\n     * @param token The optional token passed to this object's constructor.\n     */\n    void onDismiss(View view, Object token);\n  }\n\n  /**\n   * Constructs a new swipe-to-dismiss touch listener for the given view.\n   *\n   * @param view     The view to make dismissable.\n   * @param token    An optional token/cookie object to be passed through to the callback.\n   * @param callbacks The callback to trigger when the user has indicated that she would like to\n   *                 dismiss this view.\n   */\n  public SwipeDismissTouchListener(View view, Object token, DismissCallbacks callbacks) {\n    ViewConfiguration vc = ViewConfiguration.get(view.getContext());\n    mSlop = vc.getScaledTouchSlop();\n    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;\n    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();\n    mAnimationTime = view.getContext().getResources().getInteger(\n        android.R.integer.config_shortAnimTime);\n    mView = view;\n    mToken = token;\n    mCallbacks = callbacks;\n  }\n\n  @Override\n  @SuppressWarnings(\"checkstyle:operatorwrap\")\n  public boolean onTouch(View view, MotionEvent motionEvent) {\n    // offset because the view is translated during swipe\n    motionEvent.offsetLocation(mTranslationX, 0);\n\n    if (mViewWidth < 2) {\n      mViewWidth = mView.getWidth();\n    }\n\n    switch (motionEvent.getActionMasked()) {\n      case MotionEvent.ACTION_DOWN: {\n        // TODO: ensure this is a finger, and set a flag\n        mDownX = motionEvent.getRawX();\n        mDownY = motionEvent.getRawY();\n        if (mCallbacks.canDismiss(mToken)) {\n          mVelocityTracker = VelocityTracker.obtain();\n          mVelocityTracker.addMovement(motionEvent);\n        }\n        return false;\n      }\n\n      case MotionEvent.ACTION_UP: {\n        if (mVelocityTracker == null) {\n          break;\n        }\n\n        float deltaX = motionEvent.getRawX() - mDownX;\n        mVelocityTracker.addMovement(motionEvent);\n        mVelocityTracker.computeCurrentVelocity(1000);\n        float velocityX = mVelocityTracker.getXVelocity();\n        float absVelocityX = Math.abs(velocityX);\n        float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());\n        boolean dismiss = false;\n        boolean dismissRight = false;\n        if (Math.abs(deltaX) > mViewWidth / 2 && mSwiping) {\n          dismiss = true;\n          dismissRight = deltaX > 0;\n        } else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity\n            && absVelocityY < absVelocityX && mSwiping) {\n          // dismiss only if flinging in the same direction as dragging\n          dismiss = (velocityX < 0) == (deltaX < 0);\n          dismissRight = mVelocityTracker.getXVelocity() > 0;\n        }\n        if (dismiss) {\n          // dismiss\n          mView.animate()\n              .translationX(dismissRight ? mViewWidth : -mViewWidth)\n              .alpha(0)\n              .setDuration(mAnimationTime)\n              .setListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                  performDismiss();\n                }\n              });\n        } else if (mSwiping) {\n          // cancel\n          mView.animate()\n              .translationX(0)\n              .alpha(1)\n              .setDuration(mAnimationTime)\n              .setListener(null);\n        }\n        mVelocityTracker.recycle();\n        mVelocityTracker = null;\n        mTranslationX = 0;\n        mDownX = 0;\n        mDownY = 0;\n        mSwiping = false;\n        break;\n      }\n\n      case MotionEvent.ACTION_CANCEL: {\n        if (mVelocityTracker == null) {\n          break;\n        }\n\n        mView.animate()\n            .translationX(0)\n            .alpha(1)\n            .setDuration(mAnimationTime)\n            .setListener(null);\n        mVelocityTracker.recycle();\n        mVelocityTracker = null;\n        mTranslationX = 0;\n        mDownX = 0;\n        mDownY = 0;\n        mSwiping = false;\n        break;\n      }\n\n      case MotionEvent.ACTION_MOVE: {\n        if (mVelocityTracker == null) {\n          break;\n        }\n\n        mVelocityTracker.addMovement(motionEvent);\n        float deltaX = motionEvent.getRawX() - mDownX;\n        float deltaY = motionEvent.getRawY() - mDownY;\n        if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) {\n          mSwiping = true;\n          mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop);\n          mView.getParent().requestDisallowInterceptTouchEvent(true);\n\n          // Cancel listview's touch\n          MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);\n          cancelEvent.setAction(MotionEvent.ACTION_CANCEL |\n              (motionEvent.getActionIndex() <<\n                  MotionEvent.ACTION_POINTER_INDEX_SHIFT));\n          mView.onTouchEvent(cancelEvent);\n          cancelEvent.recycle();\n        }\n\n        if (mSwiping) {\n          mTranslationX = deltaX;\n          mView.setTranslationX(deltaX - mSwipingSlop);\n          // TODO: use an ease-out interpolator or such\n//                    mView.setAlpha(Math.max(0f, Math.min(1f,\n//                            1f - 2f * Math.abs(deltaX) / mViewWidth)));\n          return true;\n        }\n        break;\n      }\n    }\n    return false;\n  }\n\n  @TargetApi(12)\n  public void performDismiss() {\n    // Animate the dismissed view to zero-height and then fire the dismiss callback.\n    // This triggers layout on each animation frame; in the future we may want to do something\n    // smarter and more performant.\n\n    final ViewGroup.LayoutParams lp = mView.getLayoutParams();\n    final int originalHeight = mView.getHeight();\n\n    ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);\n\n    animator.addListener(new AnimatorListenerAdapter() {\n      @Override\n      public void onAnimationEnd(Animator animation) {\n        mCallbacks.onDismiss(mView, mToken);\n        // Reset view presentation\n        mView.setAlpha(1f);\n        mView.setTranslationX(0);\n        lp.height = originalHeight;\n        mView.setLayoutParams(lp);\n      }\n    });\n\n    animator.addUpdateListener(valueAnimator -> {\n      lp.height = (Integer) valueAnimator.getAnimatedValue();\n      mView.setLayoutParams(lp);\n    });\n\n    animator.start();\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/listeners/TouchAwareSwipeDismissTouchListener.kt",
    "content": "package com.braze.ui.inappmessage.listeners\n\nimport android.view.MotionEvent\nimport android.view.View\n\n/**\n * Adds touch events to the SwipeDismissTouchListener.\n */\nclass TouchAwareSwipeDismissTouchListener(view: View, token: Any?, callbacks: DismissCallbacks?) :\n    SwipeDismissTouchListener(view, token, callbacks) {\n    private var touchListener: ITouchListener? = null\n\n    interface ITouchListener {\n        fun onTouchStartedOrContinued()\n        fun onTouchEnded()\n    }\n\n    constructor(view: View, callbacks: DismissCallbacks?) : this(view, null, callbacks)\n\n    fun setTouchListener(newTouchListener: ITouchListener) {\n        touchListener = newTouchListener\n    }\n\n    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {\n        when (motionEvent.action) {\n            MotionEvent.ACTION_DOWN -> touchListener?.onTouchStartedOrContinued()\n            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> touchListener?.onTouchEnded()\n            else -> {}\n        }\n        return super.onTouch(view, motionEvent)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/utils/BackgroundInAppMessagePreparer.kt",
    "content": "package com.braze.ui.inappmessage.utils\n\nimport android.content.Context\nimport androidx.annotation.VisibleForTesting\nimport com.braze.Braze\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.enums.inappmessage.InAppMessageFailureType\nimport com.braze.enums.inappmessage.MessageType\nimport com.braze.images.IBrazeImageLoader\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageWithImage\nimport com.braze.models.inappmessage.IInAppMessageZippedAssetHtml\nimport com.braze.models.inappmessage.InAppMessageFull\nimport com.braze.models.inappmessage.InAppMessageHtml\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.WebContentUtils.getHtmlInAppMessageAssetCacheDirectory\nimport com.braze.support.WebContentUtils.getLocalHtmlUrlFromRemoteUrl\nimport com.braze.support.WebContentUtils.replacePrefetchedUrlsWithLocalAssets\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nobject BackgroundInAppMessagePreparer {\n    /**\n     * @param inAppMessageToPrepare The message to prepare for eventual display.\n     */\n    @JvmStatic\n    fun prepareInAppMessageForDisplay(inAppMessageToPrepare: IInAppMessage) {\n        BrazeCoroutineScope.launch {\n            try {\n                val preparedInAppMessage = prepareInAppMessage(inAppMessageToPrepare)\n                if (preparedInAppMessage == null) {\n                    brazelog(W) {\n                        \"Cannot display the in-app message because the in-app message was null.\"\n                    }\n                } else {\n                    displayPreparedInAppMessage(preparedInAppMessage)\n                }\n            } catch (e: Exception) {\n                brazelog(E, e) { \"Caught error while preparing in app message in background\" }\n            }\n        }\n    }\n\n    private fun prepareInAppMessage(inAppMessageToPrepare: IInAppMessage): IInAppMessage? {\n        if (inAppMessageToPrepare.isControl) {\n            brazelog { \"Skipping in-app message preparation for control in-app message.\" }\n            return inAppMessageToPrepare\n        }\n        brazelog { \"Starting asynchronous in-app message preparation for message.\" }\n        when (inAppMessageToPrepare.messageType) {\n            MessageType.HTML_FULL -> if (!prepareInAppMessageWithZippedAssetHtml(\n                    inAppMessageToPrepare as IInAppMessageZippedAssetHtml\n                )\n            ) {\n                brazelog(W) { \"Logging html in-app message zip asset download failure\" }\n                inAppMessageToPrepare.logDisplayFailure(InAppMessageFailureType.ZIP_ASSET_DOWNLOAD)\n                return null\n            }\n            MessageType.HTML -> prepareInAppMessageWithHtml(inAppMessageToPrepare as InAppMessageHtml)\n            else -> {\n                val didImageDownloadSucceed =\n                    prepareInAppMessageWithBitmapDownload(inAppMessageToPrepare)\n                if (!didImageDownloadSucceed) {\n                    brazelog(W) { \"Logging in-app message image download failure\" }\n                    inAppMessageToPrepare.logDisplayFailure(InAppMessageFailureType.IMAGE_DOWNLOAD)\n                    return null\n                }\n            }\n        }\n        return inAppMessageToPrepare\n    }\n\n    /**\n     * Prepares the in-app message for displaying Html content.\n     *\n     * @param inAppMessageHtml the in-app message to be prepared\n     * @return The success of the asset download.\n     */\n    @JvmStatic\n    @VisibleForTesting\n    fun prepareInAppMessageWithZippedAssetHtml(inAppMessageHtml: IInAppMessageZippedAssetHtml): Boolean {\n        // If the local assets exist already, return right away.\n        val localAssets = inAppMessageHtml.localAssetsDirectoryUrl\n        if (!localAssets.isNullOrBlank() && File(localAssets).exists()) {\n            brazelog(I) {\n                \"Local assets for html in-app message are already populated. \" +\n                    \"Not downloading assets. Location = $localAssets\"\n            }\n            return true\n        }\n\n        val assetsZipRemoteUrl = inAppMessageHtml.assetsZipRemoteUrl\n        // Otherwise, return if no remote asset zip location is specified.\n        if (assetsZipRemoteUrl.isNullOrBlank()) {\n            brazelog(I) {\n                \"Html in-app message has no remote asset zip. \" +\n                    \"Continuing with in-app message preparation.\"\n            }\n            return true\n        }\n        // Otherwise, download the asset zip.\n        val applicationContext = BrazeInAppMessageManager.getInstance().applicationContext\n        if (applicationContext == null) {\n            brazelog(W) { \"BrazeInAppMessageManager applicationContext is null. Not downloading image.\" }\n            return false\n        }\n\n        val internalStorageCacheDirectory =\n            getHtmlInAppMessageAssetCacheDirectory(applicationContext)\n        val localWebContentUrl = getLocalHtmlUrlFromRemoteUrl(\n            internalStorageCacheDirectory,\n            assetsZipRemoteUrl\n        )\n        return if (!localWebContentUrl.isNullOrBlank()) {\n            brazelog { \"Local url for html in-app message assets is $localWebContentUrl\" }\n            inAppMessageHtml.localAssetsDirectoryUrl = localWebContentUrl\n            true\n        } else {\n            brazelog(W) {\n                \"Download of html content to local directory failed for remote url: \" +\n                    \"${inAppMessageHtml.assetsZipRemoteUrl} . Returned local url is: $localWebContentUrl\"\n            }\n            false\n        }\n    }\n\n    /**\n     * Prepares the in-app message for displaying images using a bitmap downloader. The in-app\n     * message must have a valid image url.\n     *\n     * @param inAppMessage the in-app message to be prepared\n     * @return whether or not the asset download succeeded\n     */\n    @JvmStatic\n    @VisibleForTesting\n    @Suppress(\"ReturnCount\")\n    fun prepareInAppMessageWithBitmapDownload(inAppMessage: IInAppMessage?): Boolean {\n        if (inAppMessage !is IInAppMessageWithImage) {\n            brazelog { \"Cannot prepare non IInAppMessageWithImage object with bitmap download.\" }\n            return false\n        }\n        val inAppMessageWithImage = inAppMessage as IInAppMessageWithImage\n        if (inAppMessageWithImage.bitmap != null) {\n            brazelog(I) { \"In-app message already contains image bitmap. Not downloading image from URL.\" }\n            inAppMessageWithImage.imageDownloadSuccessful = true\n            return true\n        }\n        val viewBounds = getViewBoundsByType(inAppMessage)\n        val applicationContext = BrazeInAppMessageManager.getInstance().applicationContext\n        if (applicationContext == null) {\n            brazelog(W) { \"BrazeInAppMessageManager applicationContext is null. Not downloading image.\" }\n            return false\n        }\n\n        val imageLoader = Braze.getInstance(applicationContext).imageLoader\n        // Try to load the local url first\n        val localImageUrl = inAppMessageWithImage.localImageUrl\n        if (!localImageUrl.isNullOrBlank()) {\n            if (handleLocalImage(\n                    localImageUrl,\n                    inAppMessageWithImage,\n                    imageLoader,\n                    applicationContext,\n                    inAppMessage,\n                    viewBounds\n                )\n            ) return true\n        }\n\n        // Try to load the remote url next\n        val remoteImageUrl = inAppMessageWithImage.remoteImageUrl\n        if (!remoteImageUrl.isNullOrBlank()) {\n            brazelog(I) { \"In-app message has remote image url. Downloading image at url: $remoteImageUrl\" }\n            inAppMessageWithImage.bitmap = imageLoader.getInAppMessageBitmapFromUrl(\n                applicationContext,\n                inAppMessage,\n                remoteImageUrl,\n                viewBounds\n            )\n        } else {\n            brazelog(W) { \"In-app message has no remote image url. Not downloading image.\" }\n            if (inAppMessageWithImage is InAppMessageFull) {\n                brazelog(W) {\n                    \"In-app message full has no remote image url yet is required to have an image. \" +\n                        \"Failing message display.\"\n                }\n                return false\n            }\n            return true\n        }\n        if (inAppMessageWithImage.bitmap != null) {\n            inAppMessageWithImage.imageDownloadSuccessful = true\n            return true\n        }\n        return false\n    }\n\n    /**\n     * Grab the image from a local URI and set the bitmap field of the in-app message.\n     *\n     * @return Boolean if the bitmap was successfully loaded.\n     */\n    @Suppress(\"LongParameterList\")\n    private fun handleLocalImage(\n        localImageUrl: String,\n        inAppMessageWithImage: IInAppMessageWithImage,\n        imageLoader: IBrazeImageLoader,\n        applicationContext: Context,\n        inAppMessage: IInAppMessage,\n        viewBounds: BrazeViewBounds\n    ): Boolean {\n        brazelog(I) { \"Passing in-app message local image url to image loader: $localImageUrl\" }\n        inAppMessageWithImage.bitmap = imageLoader.getInAppMessageBitmapFromUrl(\n            applicationContext,\n            inAppMessage,\n            localImageUrl,\n            viewBounds\n        )\n        if (inAppMessageWithImage.bitmap != null) {\n            // Got the image, we're done\n            inAppMessageWithImage.imageDownloadSuccessful = true\n            return true\n        }\n        // The local uri didn't work, unset it from the IAM\n        brazelog {\n            \"Removing local image url from IAM since it could not be loaded. URL: $localImageUrl\"\n        }\n        inAppMessageWithImage.localImageUrl = null\n        return false\n    }\n\n    /**\n     * Prepares the in-app message for display by substituting locally\n     * cached assets into the message of the [InAppMessageHtml].\n     */\n    @VisibleForTesting\n    fun prepareInAppMessageWithHtml(inAppMessage: InAppMessageHtml) {\n        if (inAppMessage.getLocalPrefetchedAssetPaths().isEmpty()) {\n            brazelog {\n                \"HTML in-app message does not have prefetched assets. \" +\n                    \"Not performing any substitutions.\"\n            }\n            return\n        }\n        val message = inAppMessage.message\n        if (message == null) {\n            brazelog {\n                \"HTML in-app message does not have message. Not performing any substitutions.\"\n            }\n            return\n        }\n        val transformedHtml = replacePrefetchedUrlsWithLocalAssets(\n            message,\n            inAppMessage.getLocalPrefetchedAssetPaths()\n        )\n        inAppMessage.message = transformedHtml\n    }\n\n    private fun getViewBoundsByType(inAppMessage: IInAppMessage): BrazeViewBounds {\n        return when (inAppMessage.messageType) {\n            MessageType.SLIDEUP -> BrazeViewBounds.IN_APP_MESSAGE_SLIDEUP\n            MessageType.MODAL -> BrazeViewBounds.IN_APP_MESSAGE_MODAL\n            else -> BrazeViewBounds.NO_BOUNDS\n        }\n    }\n\n    private suspend fun displayPreparedInAppMessage(inAppMessage: IInAppMessage) {\n        withContext(Dispatchers.Main) {\n            this@BackgroundInAppMessagePreparer.brazelog { \"Displaying in-app message.\" }\n            BrazeInAppMessageManager.getInstance().displayInAppMessage(inAppMessage, false)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/utils/InAppMessageButtonViewUtils.kt",
    "content": "package com.braze.ui.inappmessage.utils\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.graphics.drawable.GradientDrawable\nimport android.graphics.drawable.RippleDrawable\nimport android.graphics.drawable.StateListDrawable\nimport android.os.Build\nimport android.view.View\nimport android.widget.Button\nimport com.braze.ui.R\nimport com.braze.models.inappmessage.MessageButton\n\nobject InAppMessageButtonViewUtils {\n    /**\n     * Sets the appropriate colors for the button text, background, and border.\n     *\n     * @param buttonViews    The destination views for the attributes found in the [MessageButton] objects.\n     * @param messageButtons The [MessageButton] source objects.\n     */\n    @JvmStatic\n    fun setButtons(\n        buttonViews: List<View>,\n        messageButtons: List<MessageButton>\n    ) {\n        for (i in buttonViews.indices) {\n            val buttonView = buttonViews[i]\n            val messageButton = messageButtons[i]\n            val strokeWidth = buttonView.context\n                .resources\n                .getDimensionPixelSize(R.dimen.com_braze_inappmessage_button_border_stroke)\n            val strokeFocusedWidth = buttonView.context\n                .resources\n                .getDimensionPixelSize(R.dimen.com_braze_inappmessage_button_border_stroke_focused)\n            if (messageButtons.size <= i) {\n                buttonView.visibility = View.GONE\n            } else {\n                if (buttonView is Button) {\n                    setButton(buttonView, messageButton, strokeWidth, strokeFocusedWidth)\n                }\n            }\n        }\n    }\n\n    @JvmStatic\n    fun setButton(\n        button: Button,\n        messageButton: MessageButton,\n        strokeWidth: Int,\n        strokeFocusedWidth: Int\n    ) {\n        button.text = messageButton.text\n        button.contentDescription = messageButton.text\n        InAppMessageViewUtils.setTextViewColor(button, messageButton.textColor)\n\n        // StateListDrawable is the background, holding everything else\n        val stateListDrawableBackground = StateListDrawable()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            // The rounded corners in the background give a \"shadow\" that's actually a state list animator\n            // See https://stackoverflow.com/questions/44527700/android-button-with-rounded-corners-ripple-effect-and-no-shadow\n            button.stateListAnimator = null\n        }\n        val defaultButtonDrawable =\n            getButtonDrawable(button.context, messageButton, strokeWidth, strokeFocusedWidth, false)\n        val focusedButtonDrawable =\n            getButtonDrawable(button.context, messageButton, strokeWidth, strokeFocusedWidth, true)\n\n        // The focused state MUST be added before the enabled state to work properly\n        stateListDrawableBackground.addState(\n            intArrayOf(android.R.attr.state_focused),\n            focusedButtonDrawable\n        )\n        stateListDrawableBackground.addState(\n            intArrayOf(android.R.attr.state_enabled),\n            defaultButtonDrawable\n        )\n        button.background = stateListDrawableBackground\n    }\n\n    // getDrawable() is deprecated but the alternatives are above our min SDK version of Build.VERSION_CODES.JELLY_BEAN\n    @JvmStatic\n    fun getDrawable(\n        context: Context,\n        drawableId: Int\n    ): Drawable =\n        @Suppress(\"deprecation\")\n        context.resources.getDrawable(drawableId)\n\n    @JvmStatic\n    fun getButtonDrawable(\n        context: Context,\n        messageButton: MessageButton,\n        newStrokeWidth: Int,\n        strokeFocusedWidth: Int,\n        isFocused: Boolean\n    ): Drawable {\n        val buttonDrawable = getDrawable(context, R.drawable.com_braze_inappmessage_button_background)\n        buttonDrawable.mutate()\n        val backgroundFillGradientDrawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            // The drawable pulled from resources is a ripple drawable\n            val rippleDrawable = buttonDrawable as RippleDrawable\n            rippleDrawable\n                .findDrawableByLayerId(R.id.com_braze_inappmessage_button_background_ripple_internal_gradient) as GradientDrawable\n        } else {\n            // It's just the GradientDrawable as the only element since no ripple exists\n            buttonDrawable as GradientDrawable\n        }\n        var strokeWidth = newStrokeWidth\n        if (isFocused) {\n            strokeWidth = strokeFocusedWidth\n        }\n        backgroundFillGradientDrawable.setStroke(strokeWidth, messageButton.borderColor)\n        backgroundFillGradientDrawable.setColor(messageButton.backgroundColor)\n        return buttonDrawable\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/utils/InAppMessageViewUtils.kt",
    "content": "package com.braze.ui.inappmessage.utils\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BlendMode\nimport android.graphics.BlendModeColorFilter\nimport android.graphics.Color\nimport android.graphics.PorterDuff\nimport android.graphics.Typeface\nimport android.graphics.drawable.Drawable\nimport android.graphics.drawable.GradientDrawable\nimport android.graphics.drawable.LayerDrawable\nimport android.os.Build\nimport android.view.Gravity\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.annotation.ColorInt\nimport com.braze.enums.inappmessage.TextAlign\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\n\nobject InAppMessageViewUtils {\n    @JvmStatic\n    fun setImage(bitmap: Bitmap?, imageView: ImageView) {\n        if (bitmap != null) {\n            imageView.setImageBitmap(bitmap)\n        }\n    }\n\n    @JvmStatic\n    fun setIcon(\n        context: Context,\n        icon: String?,\n        iconColor: Int,\n        iconBackgroundColor: Int,\n        textView: TextView\n    ) {\n        if (icon != null) {\n            try {\n                textView.typeface =\n                    Typeface.createFromAsset(context.assets, \"fontawesome-webfont.ttf\")\n            } catch (e: Exception) {\n                brazelog(E, e) { \"Caught exception setting icon typeface. Not rendering icon.\" }\n                return\n            }\n            textView.text = icon\n            setTextViewColor(textView, iconColor)\n            if (textView.background != null) {\n                setDrawableColor(textView.background, iconBackgroundColor)\n            } else {\n                setViewBackgroundColor(textView, iconBackgroundColor)\n            }\n        }\n    }\n\n    @JvmStatic\n    fun setFrameColor(view: View, color: Int?) {\n        color?.let { view.setBackgroundColor(it) }\n    }\n\n    @JvmStatic\n    fun setTextViewColor(textView: TextView, color: Int) {\n        textView.setTextColor(color)\n    }\n\n    @JvmStatic\n    fun setViewBackgroundColor(view: View, color: Int) {\n        view.setBackgroundColor(color)\n    }\n\n    @JvmStatic\n    fun setViewBackgroundColorFilter(view: View, @ColorInt color: Int) {\n        setDrawableColorFilter(view.background, color)\n\n        // The alpha needs to be set separately from the background color filter or else it won't apply\n        view.background.alpha = Color.alpha(color)\n    }\n\n    @JvmStatic\n    fun setDrawableColor(drawable: Drawable, @ColorInt color: Int) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            if (drawable is LayerDrawable) {\n                // This layer drawable should have the GradientDrawable as the\n                // 0th layer and the RippleDrawable as the 1st layer\n                if (drawable.numberOfLayers > 0 && drawable.getDrawable(0) is GradientDrawable) {\n                    setDrawableColor(drawable.getDrawable(0), color)\n                } else {\n                    brazelog {\n                        \"LayerDrawable for button background did not have the expected \" +\n                            \"number of layers or the 0th layer was not a GradientDrawable.\"\n                    }\n                }\n            }\n        }\n        if (drawable is GradientDrawable) {\n            drawable.setColor(color)\n        } else {\n            setDrawableColorFilter(drawable, color)\n        }\n    }\n\n    @JvmStatic\n    fun resetMessageMarginsIfNecessary(messageView: TextView?, headerView: TextView?) {\n        if (headerView == null && messageView != null) {\n            // If header is not present but message is present, reset message margins to 0\n            // Typically, the message's has a top margin to accommodate the header.\n            val layoutParams =\n                LinearLayout.LayoutParams(messageView.layoutParams.width, messageView.layoutParams.height)\n            layoutParams.setMargins(0, 0, 0, 0)\n            messageView.layoutParams = layoutParams\n        }\n    }\n\n    @JvmStatic\n    fun closeInAppMessageOnKeycodeBack() {\n        brazelog { \"Back button intercepted by in-app message view, closing in-app message.\" }\n        BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(true)\n    }\n\n    @JvmStatic\n    fun setTextAlignment(textView: TextView, textAlign: TextAlign) {\n        if (textAlign == TextAlign.START) {\n            textView.gravity = Gravity.START\n        } else if (textAlign == TextAlign.END) {\n            textView.gravity = Gravity.END\n        } else if (textAlign == TextAlign.CENTER) {\n            textView.gravity = Gravity.CENTER\n        }\n    }\n\n    @Suppress(\"deprecation\")\n    private fun setDrawableColorFilter(drawable: Drawable, @ColorInt color: Int) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            drawable.colorFilter = BlendModeColorFilter(color, BlendMode.SRC_ATOP)\n        } else {\n            drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/utils/InAppMessageWebViewClient.kt",
    "content": "package com.braze.ui.inappmessage.utils\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.webkit.RenderProcessGoneDetail\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.annotation.RequiresApi\nimport androidx.annotation.VisibleForTesting\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.getAssetFileStringContents\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.listeners.IInAppMessageWebViewClientListener\nimport com.braze.ui.inappmessage.listeners.IWebViewClientStateListener\nimport com.braze.ui.support.getQueryParameters\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.withContext\nimport java.util.concurrent.atomic.AtomicBoolean\n\nopen class InAppMessageWebViewClient(\n    private val context: Context,\n    private val inAppMessage: IInAppMessage,\n    private val inAppMessageWebViewClientListener: IInAppMessageWebViewClientListener?\n) : WebViewClient() {\n    private var webViewClientStateListener: IWebViewClientStateListener? = null\n    private var hasPageFinishedLoading = false\n    private val hasCalledPageFinishedOnListener = AtomicBoolean(false)\n    private var markPageFinishedJob: Job? = null\n\n    private val maxOnPageFinishedWaitTimeMs: Int =\n        BrazeConfigurationProvider(context).inAppMessageWebViewClientOnPageFinishedMaxWaitMs\n\n    override fun onPageFinished(view: WebView, url: String) {\n        appendBridgeJavascript(view)\n        webViewClientStateListener?.let { stateListener ->\n            if (hasCalledPageFinishedOnListener.compareAndSet(false, true)) {\n                brazelog(V) { \"Page has finished loading. Calling onPageFinished on listener\" }\n                stateListener.onPageFinished()\n            }\n        }\n        hasPageFinishedLoading = true\n\n        // Cancel any pending jobs based on the page finished wait\n        markPageFinishedJob?.cancel()\n        markPageFinishedJob = null\n    }\n\n    private fun markPageFinished() {\n        webViewClientStateListener?.let { stateListener ->\n            if (hasCalledPageFinishedOnListener.compareAndSet(false, true)) {\n                brazelog(V) {\n                    \"Page may not have finished loading, but max wait time has expired.\" +\n                        \" Calling onPageFinished on listener.\"\n                }\n                stateListener.onPageFinished()\n            }\n        }\n    }\n\n    private fun appendBridgeJavascript(view: WebView) {\n        val javascriptString: String = try {\n            context.assets.getAssetFileStringContents(BRIDGE_JS_FILE)\n        } catch (e: Exception) {\n            // Fail instead of present a broken WebView\n            BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(false)\n            brazelog(E, e) { \"Failed to get HTML in-app message javascript additions\" }\n            return\n        }\n        view.loadUrl(JAVASCRIPT_PREFIX + javascriptString)\n    }\n\n    /**\n     * Handles `appboy` schemed (\"appboy://\") urls in the HTML content WebViews. If the url isn't\n     * `appboy` schemed, then the url is passed to the attached IInAppMessageWebViewClientListener.\n     *\n     * We expect the URLs to be hierarchical and have `appboy` equal the scheme.\n     * For example, `appboy://close` is one such URL.\n     *\n     * @return true since all actions in Html In-App Messages are handled outside of the In-App Message itself.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) =\n        handleUrlOverride(request.url.toString())\n\n    override fun shouldOverrideUrlLoading(view: WebView, url: String) = handleUrlOverride(url)\n\n    fun setWebViewClientStateListener(listener: IWebViewClientStateListener?) {\n        // If the page is already done loading, inform the new listener\n        if (listener != null &&\n            hasPageFinishedLoading &&\n            hasCalledPageFinishedOnListener.compareAndSet(false, true)\n        ) {\n            listener.onPageFinished()\n        } else {\n            markPageFinishedJob = BrazeCoroutineScope.launchDelayed(maxOnPageFinishedWaitTimeMs) {\n                withContext(Dispatchers.Main) {\n                    markPageFinished()\n                }\n            }\n        }\n        webViewClientStateListener = listener\n    }\n\n    private fun handleUrlOverride(url: String): Boolean {\n        if (inAppMessageWebViewClientListener == null) {\n            brazelog(I) { \"InAppMessageWebViewClient was given null IInAppMessageWebViewClientListener listener. Returning true.\" }\n            return true\n        }\n        if (url.isBlank()) {\n            // Blank urls shouldn't be passed back to the WebView. We return true here to indicate\n            // to the WebView that we handled the url.\n            brazelog(I) { \"InAppMessageWebViewClient.shouldOverrideUrlLoading was given blank url. Returning true.\" }\n            return true\n        }\n        val uri = Uri.parse(url)\n        val queryBundle = getBundleFromUrl(url)\n        if (uri.scheme != null && uri.scheme == BRAZE_INAPP_MESSAGE_SCHEME) {\n            // Check the authority\n            when (uri.authority) {\n                null -> brazelog { \"Uri authority was null. Uri: $uri\" }\n                AUTHORITY_NAME_CLOSE -> inAppMessageWebViewClientListener.onCloseAction(\n                    inAppMessage,\n                    url,\n                    queryBundle\n                )\n                AUTHORITY_NAME_NEWSFEED -> inAppMessageWebViewClientListener.onNewsfeedAction(\n                    inAppMessage,\n                    url,\n                    queryBundle\n                )\n                AUTHORITY_NAME_CUSTOM_EVENT -> inAppMessageWebViewClientListener.onCustomEventAction(\n                    inAppMessage,\n                    url,\n                    queryBundle\n                )\n            }\n            return true\n        } else {\n            brazelog { \"Uri scheme was null or not an appboy url. Uri: $uri\" }\n        }\n        inAppMessageWebViewClientListener.onOtherUrlAction(inAppMessage, url, queryBundle)\n        return true\n    }\n\n    override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {\n        brazelog(I) { \"The webview rendering process crashed, returning true\" }\n\n        // The app crashes after detecting the renderer crashed. Returning true to avoid app crash.\n        return true\n    }\n\n    companion object {\n        private const val BRIDGE_JS_FILE = \"braze-html-in-app-message-bridge.js\"\n\n        private const val BRAZE_INAPP_MESSAGE_SCHEME = \"appboy\"\n        private const val AUTHORITY_NAME_CLOSE = \"close\"\n        private const val AUTHORITY_NAME_NEWSFEED = \"feed\"\n        private const val AUTHORITY_NAME_CUSTOM_EVENT = \"customEvent\"\n\n        /**\n         * The query key for the button id for tracking.\n         */\n        const val QUERY_NAME_BUTTON_ID = \"abButtonId\"\n\n        /**\n         * The query key for opening links externally (i.e. outside your app). Url intents will be opened with\n         * the INTENT.ACTION_VIEW intent. Links beginning with the appboy:// scheme are unaffected by this query key.\n         */\n        const val QUERY_NAME_EXTERNAL_OPEN = \"abExternalOpen\"\n\n        /**\n         * Query key for directing Braze to open Url intents using the INTENT.ACTION_VIEW.\n         */\n        const val QUERY_NAME_DEEPLINK = \"abDeepLink\"\n        const val JAVASCRIPT_PREFIX = \"javascript:\"\n\n        /**\n         * Returns the string mapping of the query keys and values from the query string of the url. If the query string\n         * contains duplicate keys, then the last key in the string will be kept.\n         *\n         * @param url the url\n         * @return a bundle containing the key/value mapping of the query string. Will not be null.\n         */\n        @JvmStatic\n        @VisibleForTesting\n        fun getBundleFromUrl(url: String): Bundle {\n            val queryBundle = Bundle()\n            if (url.isBlank()) {\n                return queryBundle\n            }\n            val uri = Uri.parse(url)\n\n            uri.getQueryParameters().forEach { entry ->\n                queryBundle.putString(entry.key, entry.value)\n            }\n\n            return queryBundle\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/IInAppMessageImageView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport com.braze.enums.inappmessage.CropType\n\n/**\n * IInAppMessageImageView is a unifying interface for [android.view.View] implementations\n * that hold in-app message images, defining the required radius and cropping behavior for in-app\n * messages images.\n */\ninterface IInAppMessageImageView {\n    /**\n     * Instruct the view to use the given radii for its corners.\n     *\n     * @param topLeft top-left corner radius in px\n     * @param topRight top-right corner radius in px\n     * @param bottomLeft bottom-left corner radius in px\n     * @param bottomRight bottom-right corner radius in px\n     */\n    fun setCornersRadiiPx(topLeft: Float, topRight: Float, bottomLeft: Float, bottomRight: Float)\n\n    /**\n     * Instruct the view to use the given radius for its corners.\n     *\n     * @param cornersRadius radius for all corners in px\n     */\n    fun setCornersRadiusPx(cornersRadius: Float)\n\n    /**\n     * Instruct the view to use [android.widget.ImageView.ScaleType.CENTER_CROP] or equivalent.\n     */\n    fun setInAppMessageImageCropType(cropType: CropType?)\n    fun setAspectRatio(aspectRatio: Float)\n    fun setToHalfParentHeight(setToHalfHeight: Boolean)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/IInAppMessageImmersiveView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.view.View\n\n/**\n * [IInAppMessageImmersiveView] is the base view interface for all immersive in-app messages.\n *\n *\n * An immersive in-app message is defined as an in-app message that takes up the entire screen\n * and/or 'blocks' the user from interacting with the app until the message is dismissed.\n * Immersive views extend the base in-app message view with header text, message buttons,\n * and a close button.\n */\ninterface IInAppMessageImmersiveView : IInAppMessageView {\n    /**\n     * Gets the close button View so that Braze can add click listeners to it.\n     *\n     * @return the child View that displays the close button.\n     */\n    val messageCloseButtonView: View?\n\n    /**\n     * Gets the message button Views so that Braze can add click listeners to them.\n     *\n     * @param numButtons The number of visible buttons\n     * @return the child Views that display the message buttons. They should\n     * be returned in the same order as the List<MessageButton> on the in-app message\n     * object so that listeners are set correctly.\n     */\n    fun getMessageButtonViews(numButtons: Int): List<View>\n\n    /**\n     * Sets up the directional navigation pointers needed to support d-pad/TV-remote\n     * navigation of the in-app message.\n     *\n     * See https://developer.android.com/training/keyboard-input/navigation#Direction\n     *\n     * @param numButtons The number of [MessageButton]'s\n     * on this message.\n     */\n    fun setupDirectionalNavigation(numButtons: Int)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/IInAppMessageView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.view.View\nimport androidx.core.view.WindowInsetsCompat\n\n/**\n * [IInAppMessageView] is the base view interface for all in-app messages.\n */\ninterface IInAppMessageView {\n    /**\n     * Gets the clickable portion of the in-app message so that Braze can add click listeners to it.\n     *\n     * @return the View that displays the clickable portion of the in-app message.\n     * If the entire message is clickable, return this.\n     */\n    val messageClickableView: View?\n\n    /**\n     * Variable to prevent [WindowInsetsCompat] from getting applied\n     * multiple times on the same in-app message view.\n     *\n     * @see [applyWindowInsets]\n     * @return Whether [WindowInsetsCompat] has been applied to this in-app message.\n     */\n    var hasAppliedWindowInsets: Boolean\n\n    /**\n     * Called when the [WindowInsetsCompat] information should be applied to this\n     * in-app message. [WindowInsetsCompat] will typically only be applied on notched\n     * devices and on Activities displaying inside the screen cutout area.\n     * Implementations of this method are expected to modify any necessary margins to ensure\n     * compatibility with the argument notch dimensions. For example, full screen in-app messages\n     * should have their close buttons moved to not be obscured by the status bar, slideups should not\n     * render behind the notch, and modal in-app messages will have no changes since they do not render\n     * in the cutout area.\n     * The screen has a notch if [WindowInsetsCompat.getDisplayCutout] is non-null.\n     * The system window insets (e.g. [WindowInsetsCompat.getSystemWindowInsetTop]\n     * will be present if the status bar is translucent or the status/navigation bars are otherwise\n     * non-occluding of the root Activity content.\n     *\n     * @param insets The [WindowInsetsCompat] object directly from\n     * [androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener].\n     */\n    fun applyWindowInsets(insets: WindowInsetsCompat)\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageBaseView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport androidx.core.view.WindowInsetsCompat\nimport com.braze.enums.inappmessage.TextAlign\nimport com.braze.models.inappmessage.IInAppMessageWithImage\nimport com.braze.support.BrazeLogger.Priority.D\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setIcon\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setImage\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setTextAlignment\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setTextViewColor\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColor\nimport com.braze.ui.support.removeViewFromParent\nimport java.io.File\n\nabstract class InAppMessageBaseView(context: Context?, attrs: AttributeSet?) :\n    RelativeLayout(context, attrs), IInAppMessageView {\n    override val messageClickableView: View?\n        get() = this\n\n    override var hasAppliedWindowInsets: Boolean = false\n\n    abstract val messageTextView: TextView?\n    abstract val messageImageView: ImageView?\n    abstract val messageIconView: TextView?\n    abstract val messageBackgroundObject: Any?\n\n    open fun setMessageBackgroundColor(color: Int) {\n        setViewBackgroundColor(messageBackgroundObject as View, color)\n    }\n\n    open fun setMessageTextColor(color: Int) {\n        messageTextView?.let { setTextViewColor(it, color) }\n    }\n\n    open fun setMessageTextAlign(textAlign: TextAlign) {\n        messageTextView?.let { setTextAlignment(it, textAlign) }\n    }\n\n    open fun setMessage(text: String) {\n        messageTextView?.text = text\n    }\n\n    open fun setMessageImageView(bitmap: Bitmap) {\n        messageImageView?.let { setImage(bitmap, it) }\n    }\n\n    open fun setMessageIcon(icon: String, iconColor: Int, iconBackgroundColor: Int) {\n        messageIconView?.let { setIcon(context, icon, iconColor, iconBackgroundColor, it) }\n    }\n\n    open fun resetMessageMargins(imageRetrievalSuccessful: Boolean) {\n        messageImageView?.let {\n            if (!imageRetrievalSuccessful) {\n                it.removeViewFromParent()\n            } else {\n                messageIconView.removeViewFromParent()\n            }\n        }\n        if (messageIconView?.text?.toString()?.isBlank() == true) {\n            messageIconView.removeViewFromParent()\n        }\n    }\n\n    override fun applyWindowInsets(insets: WindowInsetsCompat) {\n        hasAppliedWindowInsets = true\n    }\n\n    companion object {\n        /**\n         * @return return the local image Url, if present. Otherwise, return the remote image Url. Local\n         * image Urls are Urls for images pre-fetched by the SDK for triggers.\n         */\n        @JvmStatic\n        fun getAppropriateImageUrl(inAppMessage: IInAppMessageWithImage): String? {\n            val localImagePath = inAppMessage.localImageUrl\n            if (!localImagePath.isNullOrBlank()) {\n                val imageFile = File(localImagePath)\n                if (imageFile.exists()) {\n                    return localImagePath\n                } else {\n                    brazelog(D) {\n                        \"Local bitmap file does not exist. Using remote url instead. Local path: $localImagePath\"\n                    }\n                }\n            }\n            return inAppMessage.remoteImageUrl\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageBoundedLayout.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.RelativeLayout\nimport com.braze.ui.R\n\n/**\n * A [RelativeLayout] that respects maximum/minimum dimension bounds.\n */\nopen class InAppMessageBoundedLayout : RelativeLayout {\n    private var maxDefinedWidthPixels = 0\n    private var minDefinedWidthPixels = 0\n    private var maxDefinedHeightPixels = 0\n    private var minDefinedHeightPixels = 0\n\n    constructor(context: Context) : super(context)\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {\n        val attributes =\n            context.obtainStyledAttributes(attrs, R.styleable.InAppMessageBoundedLayout)\n        maxDefinedWidthPixels = attributes.getDimensionPixelSize(\n            R.styleable.InAppMessageBoundedLayout_inAppMessageBoundedLayoutMaxWidth,\n            0\n        )\n        minDefinedWidthPixels = attributes.getDimensionPixelSize(\n            R.styleable.InAppMessageBoundedLayout_inAppMessageBoundedLayoutMinWidth,\n            0\n        )\n        maxDefinedHeightPixels = attributes.getDimensionPixelSize(\n            R.styleable.InAppMessageBoundedLayout_inAppMessageBoundedLayoutMaxHeight,\n            0\n        )\n        minDefinedHeightPixels = attributes.getDimensionPixelSize(\n            R.styleable.InAppMessageBoundedLayout_inAppMessageBoundedLayoutMinHeight,\n            0\n        )\n        attributes.recycle()\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        var newWidthMeasureSpec = widthMeasureSpec\n        var newHeightMeasureSpec = heightMeasureSpec\n\n        val measuredWidth = MeasureSpec.getSize(newWidthMeasureSpec)\n        if (minDefinedWidthPixels > 0 && measuredWidth < minDefinedWidthPixels) {\n            val measureMode = MeasureSpec.getMode(newWidthMeasureSpec)\n            newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(minDefinedWidthPixels, measureMode)\n        } else if (maxDefinedWidthPixels > 0 && measuredWidth > maxDefinedWidthPixels) {\n            val measureMode = MeasureSpec.getMode(newWidthMeasureSpec)\n            newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxDefinedWidthPixels, measureMode)\n        }\n\n        val measuredHeight = MeasureSpec.getSize(newHeightMeasureSpec)\n        if (minDefinedHeightPixels > 0 && measuredHeight < minDefinedHeightPixels) {\n            val measureMode = MeasureSpec.getMode(newHeightMeasureSpec)\n            newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(minDefinedHeightPixels, measureMode)\n        } else if (maxDefinedHeightPixels > 0 && measuredHeight > maxDefinedHeightPixels) {\n            val measureMode = MeasureSpec.getMode(newHeightMeasureSpec)\n            newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxDefinedHeightPixels, measureMode)\n        }\n        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageButton.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.Button\n\nopen class InAppMessageButton : Button {\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context,\n        attrs,\n        defStyleAttr\n    )\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageFullView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.app.Activity\nimport android.content.Context\nimport android.graphics.drawable.GradientDrawable\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.core.view.WindowInsetsCompat\nimport com.braze.ui.R\nimport com.braze.enums.inappmessage.ImageStyle\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams.modalizedImageRadiusDp\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColor\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColorFilter\nimport com.braze.ui.support.convertDpToPixels\nimport com.braze.ui.support.getMaxSafeBottomInset\nimport com.braze.ui.support.getMaxSafeLeftInset\nimport com.braze.ui.support.getMaxSafeRightInset\nimport com.braze.ui.support.getMaxSafeTopInset\nimport com.braze.ui.support.isRunningOnTablet\n\nopen class InAppMessageFullView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageImmersiveBaseView(context, attrs) {\n    private var inAppMessageImageView: InAppMessageImageView? = null\n    private var isGraphic = false\n\n    override val messageTextView: TextView\n        get() = findViewById(R.id.com_braze_inappmessage_full_message)\n    override val messageHeaderTextView: TextView\n        get() = findViewById(R.id.com_braze_inappmessage_full_header_text)\n    override val frameView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_full_frame)\n    override val messageCloseButtonView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_full_close_button)\n    override val messageClickableView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_full)\n    override val messageImageView: ImageView?\n        get() = inAppMessageImageView\n    override val messageIconView: TextView?\n        get() = null\n    override val messageBackgroundObject: View?\n        get() = findViewById(R.id.com_braze_inappmessage_full)\n\n    /**\n     * @return the size in pixels of the long edge of a modalized full in-app messages, used to size\n     * modalized in-app messages appropriately on tablets.\n     */\n    open val longEdge: Int\n        get() {\n            val inAppMessageFullView = findViewById<View>(R.id.com_braze_inappmessage_full)\n            return inAppMessageFullView.layoutParams.height\n        }\n\n    /**\n     * @return the size in pixels of the short edge of a modalized full in-app messages, used to size\n     * modalized in-app messages appropriately on tablets.\n     */\n    open val shortEdge: Int\n        get() {\n            val inAppMessageFullView = findViewById<View>(R.id.com_braze_inappmessage_full)\n            return inAppMessageFullView.layoutParams.width\n        }\n\n    open fun createAppropriateViews(\n        activity: Activity,\n        inAppMessage: IInAppMessageImmersive,\n        isGraphic: Boolean\n    ) {\n        inAppMessageImageView = findViewById(R.id.com_braze_inappmessage_full_imageview)\n        inAppMessageImageView?.let {\n            setInAppMessageImageViewAttributes(activity, inAppMessage, it)\n        }\n        this.isGraphic = isGraphic\n    }\n\n    override fun setMessageBackgroundColor(color: Int) {\n        val msgBackgroundObject = messageBackgroundObject\n        if (msgBackgroundObject?.background is GradientDrawable) {\n            setViewBackgroundColorFilter(msgBackgroundObject, color)\n        } else {\n            if (isGraphic) {\n                super.setMessageBackgroundColor(color)\n            } else {\n                setViewBackgroundColor(\n                    findViewById(R.id.com_braze_inappmessage_full_all_content_parent),\n                    color\n                )\n                setViewBackgroundColor(\n                    findViewById(R.id.com_braze_inappmessage_full_text_and_button_content_parent),\n                    color\n                )\n            }\n        }\n    }\n\n    override fun getMessageButtonViews(numButtons: Int): List<View> {\n        val buttonViews = mutableListOf<View>()\n\n        // Based on the number of buttons, make one of the button parent layouts visible\n        if (numButtons == 1) {\n            val singleButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_full_button_layout_single)\n            if (singleButtonParent != null) {\n                singleButtonParent.visibility = VISIBLE\n            }\n            val singleButton =\n                findViewById<View>(R.id.com_braze_inappmessage_full_button_single_one)\n            if (singleButton != null) {\n                buttonViews.add(singleButton)\n            }\n        } else if (numButtons == 2) {\n            val dualButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_full_button_layout_dual)\n            if (dualButtonParent != null) {\n                dualButtonParent.visibility = VISIBLE\n            }\n            val dualButton1 = findViewById<View>(R.id.com_braze_inappmessage_full_button_dual_one)\n            val dualButton2 = findViewById<View>(R.id.com_braze_inappmessage_full_button_dual_two)\n            if (dualButton1 != null) {\n                buttonViews.add(dualButton1)\n            }\n            if (dualButton2 != null) {\n                buttonViews.add(dualButton2)\n            }\n        }\n        return buttonViews\n    }\n\n    override fun resetMessageMargins(imageRetrievalSuccessful: Boolean) {\n        super.resetMessageMargins(imageRetrievalSuccessful)\n\n        messageClickableView?.let { msgClickableView: View ->\n            // Make scrollView pass click events to message clickable view, so that clicking on the scrollView\n            // dismisses the in-app message.\n            val scrollViewChild = findViewById<View>(R.id.com_braze_inappmessage_full_text_layout)\n            scrollViewChild.setOnClickListener {\n                brazelog { \"Passing scrollView click event to message clickable view.\" }\n                msgClickableView.performClick()\n            }\n        }\n    }\n\n    /**\n     * Applies the [WindowInsetsCompat] by ensuring the close button and message text on the in-app message does not render\n     * in the display cutout area.\n     *\n     * @param insets The [WindowInsetsCompat] object directly from\n     * [androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener].\n     */\n    override fun applyWindowInsets(insets: WindowInsetsCompat) {\n        super.applyWindowInsets(insets)\n        // Attempt to fix the close button\n        messageCloseButtonView?.let {\n            applyDisplayCutoutMarginsToCloseButton(insets, it)\n        }\n        if (isGraphic) {\n            // Fix the button layouts individually\n            val singleButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_full_button_layout_single)\n            if (singleButtonParent?.visibility == VISIBLE) {\n                applyDisplayCutoutMarginsToContentArea(insets, singleButtonParent)\n                return\n            }\n            val dualButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_full_button_layout_dual)\n            if (dualButtonParent?.visibility == VISIBLE) {\n                applyDisplayCutoutMarginsToContentArea(insets, dualButtonParent)\n            }\n        } else {\n            // Fix the content area as well. The content area is the header, message, and buttons.\n            val contentArea =\n                findViewById<View>(R.id.com_braze_inappmessage_full_text_and_button_content_parent)\n            contentArea?.let { applyDisplayCutoutMarginsToContentArea(insets, it) }\n        }\n    }\n\n    /**\n     * Programmatically set attributes on the image view classes inside the image ViewStubs.\n     */\n    private fun setInAppMessageImageViewAttributes(\n        activity: Activity,\n        inAppMessage: IInAppMessageImmersive,\n        inAppMessageImageView: IInAppMessageImageView\n    ) {\n        inAppMessageImageView.setInAppMessageImageCropType(inAppMessage.cropType)\n        if (activity.isRunningOnTablet()) {\n            val radiusInPx = convertDpToPixels(activity, modalizedImageRadiusDp).toFloat()\n            if (inAppMessage.imageStyle == ImageStyle.GRAPHIC) {\n                // for graphic fulls, set the image radius at all four corners.\n                inAppMessageImageView.setCornersRadiusPx(radiusInPx)\n            } else {\n                // for graphic fulls, set the image radius only at the top left and right corners, which\n                // are at the edge of the in-app message.\n                inAppMessageImageView.setCornersRadiiPx(radiusInPx, radiusInPx, 0.0f, 0.0f)\n            }\n        } else {\n            inAppMessageImageView.setCornersRadiusPx(0.0f)\n        }\n    }\n\n    /**\n     * Shifts/margins the close button out of the display cutout area.\n     */\n    private fun applyDisplayCutoutMarginsToCloseButton(\n        windowInsets: WindowInsetsCompat,\n        closeButtonView: View\n    ) {\n        if (closeButtonView.layoutParams == null || closeButtonView.layoutParams !is MarginLayoutParams) {\n            brazelog {\n                \"Close button layout params are null or not of the expected class. Not applying window insets.\"\n            }\n            return\n        }\n\n        // Offset the existing margin with whatever the inset margins safe area values are\n        val layoutParams = closeButtonView.layoutParams as MarginLayoutParams\n        layoutParams.setMargins(\n            getMaxSafeLeftInset(windowInsets) + layoutParams.leftMargin,\n            getMaxSafeTopInset(windowInsets) + layoutParams.topMargin,\n            getMaxSafeRightInset(windowInsets) + layoutParams.rightMargin,\n            getMaxSafeBottomInset(windowInsets) + layoutParams.bottomMargin\n        )\n    }\n\n    /**\n     * Shifts/margins the close button out of the display cutout area.\n     */\n    private fun applyDisplayCutoutMarginsToContentArea(\n        windowInsets: WindowInsetsCompat,\n        contentAreaView: View\n    ) {\n        if (contentAreaView.layoutParams !is MarginLayoutParams) {\n            brazelog {\n                \"Content area layout params are null or not of the expected class. Not applying window insets.\"\n            }\n            return\n        }\n\n        // Offset the existing margin with whatever the inset margins safe area values are\n        val layoutParams = contentAreaView.layoutParams as MarginLayoutParams\n        layoutParams.setMargins(\n            getMaxSafeLeftInset(windowInsets) + layoutParams.leftMargin,\n            layoutParams.topMargin,\n            getMaxSafeRightInset(windowInsets) + layoutParams.rightMargin,\n            getMaxSafeBottomInset(windowInsets) + layoutParams.bottomMargin\n        )\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageHtmlBaseView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Color\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.View\nimport android.webkit.ConsoleMessage\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport android.widget.RelativeLayout\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.webkit.WebSettingsCompat\nimport androidx.webkit.WebViewFeature\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.listeners.IWebViewClientStateListener\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.closeInAppMessageOnKeycodeBack\nimport com.braze.ui.inappmessage.utils.InAppMessageWebViewClient\nimport com.braze.ui.support.getMaxSafeBottomInset\nimport com.braze.ui.support.getMaxSafeLeftInset\nimport com.braze.ui.support.getMaxSafeRightInset\nimport com.braze.ui.support.getMaxSafeTopInset\nimport com.braze.ui.support.isDeviceInNightMode\nimport com.braze.ui.support.setFocusableInTouchModeAndRequestFocus\n\nabstract class InAppMessageHtmlBaseView(context: Context?, attrs: AttributeSet?) :\n    RelativeLayout(context, attrs), IInAppMessageView {\n\n    /**\n     * This should be accessed through [messageWebView] to ensure that the [WebView]\n     * is configured with all the various settings once and only once.\n     */\n    private var configuredMessageWebView: WebView? = null\n    private var inAppMessageWebViewClient: InAppMessageWebViewClient? = null\n    private var isFinished = false\n    override var hasAppliedWindowInsets: Boolean = false\n\n    override val messageClickableView: View?\n        get() {\n            return this\n        }\n\n    @get:SuppressLint(\"SetJavaScriptEnabled\")\n    open val messageWebView: WebView?\n        get() {\n            if (isFinished) {\n                brazelog(W) { \"Cannot return the WebView for an already finished message\" }\n                return null\n            }\n            val webViewViewId = getWebViewViewId()\n            if (webViewViewId == 0) {\n                brazelog { \"Cannot find WebView. getWebViewViewId() returned 0.\" }\n                return null\n            }\n            if (configuredMessageWebView != null) {\n                return configuredMessageWebView\n            }\n            val webView: WebView? = findViewById(webViewViewId)\n            if (webView == null) {\n                brazelog { \"findViewById for $webViewViewId returned null. Returning null for WebView.\" }\n                return null\n            }\n            val webSettings = webView.settings\n            webSettings.javaScriptEnabled = true\n            webSettings.useWideViewPort = true\n            webSettings.loadWithOverviewMode = true\n            webSettings.displayZoomControls = false\n            webSettings.domStorageEnabled = true\n            // Needed since locally downloaded assets are under `file://` schemes\n            webSettings.allowFileAccess = true\n            // This enables hardware acceleration if the manifest also has it defined.\n            // If not defined, then the layer type will fallback to software.\n            webView.setLayerType(LAYER_TYPE_HARDWARE, null)\n            webView.setBackgroundColor(Color.TRANSPARENT)\n            try {\n                // Note that this check is OS version agnostic since the Android WebView can be\n                // updated independently\n                if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)\n                    && isDeviceInNightMode(context)\n                ) {\n                    WebSettingsCompat.setForceDark(\n                        webSettings,\n                        WebSettingsCompat.FORCE_DARK_ON\n                    )\n                }\n                if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {\n                    WebSettingsCompat.setForceDarkStrategy(\n                        webSettings,\n                        WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY\n                    )\n                }\n            } catch (e: Throwable) {\n                brazelog(E, e) { \"Failed to set dark mode WebView settings\" }\n            }\n\n            // Set the client for console logging. See https://developer.android.com/guide/webapps/debugging.html\n            webView.webChromeClient = object : WebChromeClient() {\n                override fun onConsoleMessage(cm: ConsoleMessage): Boolean {\n                    this@InAppMessageHtmlBaseView.brazelog {\n                        (\n                            \"Braze HTML In-app Message log. Line: \" + cm.lineNumber()\n                                + \". SourceId: \" + cm.sourceId()\n                                + \". Log Level: \" + cm.messageLevel()\n                                + \". Message: \" + cm.message()\n                            )\n                    }\n                    return true\n                }\n\n                override fun getDefaultVideoPoster() =\n                    // This bitmap is used to eliminate the default black & white\n                    // play icon used as the default poster.\n                    Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)\n            }\n            configuredMessageWebView = webView\n            return configuredMessageWebView\n        }\n\n    /**\n     * Should be called when the held [WebView] of this class is\n     * done displaying its message. Future calls to\n     * [messageWebView] will return null afterwards.\n     */\n    open fun finishWebViewDisplay() {\n        brazelog { \"Finishing WebView display\" }\n        // Note that WebView.destroy() is not called here since that\n        // causes immense issues with the system's own closing of\n        // the WebView after we're done with it.\n        isFinished = true\n        configuredMessageWebView?.let {\n            it.loadUrl(FINISHED_WEBVIEW_URL)\n            it.onPause()\n            it.removeAllViews()\n            configuredMessageWebView = null\n        }\n    }\n\n    /**\n     * Loads the WebView using an html string and local file resource url. This url should be a path\n     * to a file on the local filesystem.\n     *\n     * @param htmlBody          Html text encoded in utf-8\n     * @param assetDirectoryUrl path to the local assets file\n     */\n    @JvmOverloads\n    open fun setWebViewContent(htmlBody: String?, assetDirectoryUrl: String? = null) {\n        // File URIs must be loaded with this \"file://\" scheme\n        // since our html might have mixed http/data/file content\n        // See https://developer.android.com/reference/android/webkit/WebView#loadData(java.lang.String,%20java.lang.String,%20java.lang.String)\n        if (htmlBody != null) {\n            messageWebView?.loadDataWithBaseURL(\n                \"$FILE_URI_SCHEME_PREFIX$assetDirectoryUrl/\",\n                htmlBody,\n                HTML_MIME_TYPE,\n                HTML_ENCODING,\n                null\n            )\n        } else {\n            brazelog { \"Cannot load WebView. htmlBody was null.\" }\n        }\n    }\n\n    open fun setInAppMessageWebViewClient(inAppMessageWebViewClient: InAppMessageWebViewClient) {\n        messageWebView?.webViewClient = inAppMessageWebViewClient\n        this.inAppMessageWebViewClient = inAppMessageWebViewClient\n    }\n\n    open fun setHtmlPageFinishedListener(listener: IWebViewClientStateListener?) {\n        inAppMessageWebViewClient?.setWebViewClientStateListener(listener)\n    }\n\n    /**\n     * Html in-app messages can alternatively be closed by the back button.\n     *\n     * Note: If the internal WebView has focus instead of this view, back button events on html\n     * in-app messages are handled separately in [InAppMessageWebView.onKeyDown]\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        if (keyCode == KeyEvent.KEYCODE_BACK && BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        configuredMessageWebView?.setFocusableInTouchModeAndRequestFocus()\n        return super.onKeyDown(keyCode, event)\n    }\n\n    /**\n     * Returns the [View.getId] used in the\n     * default [InAppMessageHtmlBaseView.messageWebView]\n     * implementation.\n     *\n     * @return The [View.getId] for the [WebView] backing this message.\n     */\n    abstract fun getWebViewViewId(): Int\n\n    /**\n     * HTML messages can alternatively be closed by the back button.\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun dispatchKeyEvent(event: KeyEvent): Boolean {\n        if (!isInTouchMode && event.keyCode == KeyEvent.KEYCODE_BACK && BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        return super.dispatchKeyEvent(event)\n    }\n\n    override fun applyWindowInsets(insets: WindowInsetsCompat) {\n        hasAppliedWindowInsets = true\n        if (!BrazeConfigurationProvider(this.context).isHtmlInAppMessageApplyWindowInsetsEnabled) {\n            return\n        }\n        if (layoutParams == null || layoutParams !is MarginLayoutParams) {\n            return\n        }\n\n        // Offset the existing margin with whatever the inset margins safe area values are\n        val layoutParams = layoutParams as MarginLayoutParams\n        layoutParams.setMargins(\n            getMaxSafeLeftInset(insets) + layoutParams.leftMargin,\n            getMaxSafeTopInset(insets) + layoutParams.topMargin,\n            getMaxSafeRightInset(insets) + layoutParams.rightMargin,\n            getMaxSafeBottomInset(insets) + layoutParams.bottomMargin\n        )\n    }\n\n    companion object {\n        private const val HTML_MIME_TYPE = \"text/html\"\n        private const val HTML_ENCODING = \"utf-8\"\n        private const val FILE_URI_SCHEME_PREFIX = \"file://\"\n\n        /**\n         * A url for the [WebView] to load when display is finished.\n         */\n        private const val FINISHED_WEBVIEW_URL = \"about:blank\"\n\n        const val BRAZE_BRIDGE_PREFIX = \"brazeInternalBridge\"\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageHtmlFullView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.braze.ui.R\n\nopen class InAppMessageHtmlFullView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageHtmlBaseView(context, attrs) {\n\n    override fun getWebViewViewId(): Int = R.id.com_braze_inappmessage_html_full_webview\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageHtmlView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.braze.ui.R\n\nopen class InAppMessageHtmlView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageHtmlBaseView(context, attrs) {\n\n    override fun getWebViewViewId(): Int = R.id.com_braze_inappmessage_html_webview\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageImageView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Path\nimport android.graphics.RectF\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.ImageView\nimport com.braze.enums.inappmessage.CropType\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport kotlin.math.min\n\n/**\n * Extends ImageView with the ability to clip the view's corners by a defined radius on all image\n * types.\n */\n@SuppressLint(\"AppCompatCustomView\")\nclass InAppMessageImageView(context: Context?, attrs: AttributeSet?) :\n    ImageView(context, attrs),\n    IInAppMessageImageView {\n    /**\n     * Clip path that will be set to a closed round-rectangle contour based on the radii in\n     * [.inAppRadii] and used to clip the image view.\n     */\n    var clipPath: Path = Path()\n\n    /**\n     * Represents the dimensions of the image view which will be used to create the clip path.\n     */\n    var rectf: RectF = RectF()\n\n    /**\n     * Array of 8 values, 4 pairs of [X,Y] radii. Each corner receives\n     * two radius values [X, Y]. The corners are ordered top-left, top-right,\n     * bottom-right, bottom-left\n     */\n    lateinit var inAppRadii: FloatArray\n        private set\n    private var aspectRatio = -1f\n    @Suppress(\"BooleanPropertyNaming\")\n    private var setToHalfParentHeight = false\n\n    init {\n        // The view bounds need to be adjusted in order to scale to the full width available\n        adjustViewBounds = true\n    }\n\n    override fun setCornersRadiiPx(\n        topLeft: Float,\n        topRight: Float,\n        bottomLeft: Float,\n        bottomRight: Float\n    ) {\n        inAppRadii = floatArrayOf(\n            topLeft, topLeft,\n            topRight, topRight,\n            bottomLeft, bottomLeft,\n            bottomRight, bottomRight\n        )\n    }\n\n    override fun setCornersRadiusPx(cornersRadius: Float) {\n        setCornersRadiiPx(cornersRadius, cornersRadius, cornersRadius, cornersRadius)\n    }\n\n    override fun setInAppMessageImageCropType(cropType: CropType?) {\n        if (cropType == CropType.FIT_CENTER) {\n            scaleType = ScaleType.FIT_CENTER\n        } else if (cropType == CropType.CENTER_CROP) {\n            scaleType = ScaleType.CENTER_CROP\n        }\n    }\n\n    override fun setAspectRatio(aspectRatio: Float) {\n        this.aspectRatio = aspectRatio\n        requestLayout()\n    }\n\n    override fun setToHalfParentHeight(setToHalfHeight: Boolean) {\n        setToHalfParentHeight = setToHalfHeight\n        requestLayout()\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        clipCanvasToPath(canvas, width, height)\n        super.onDraw(canvas)\n    }\n\n    @Suppress(\"MagicNumber\")\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n\n        // If the View isn't large enough, don't set the aspect ratio. This will prevent Glide from\n        // applying any images to a View with 1 pixel dimensions.\n        if (aspectRatio != -1f && measuredHeight > 0 && measuredWidth > 0) {\n            val newWidth = measuredWidth\n            val maxHeight = (newWidth / aspectRatio).toInt()\n            // The +1 is necessary to ensure that the image hits the full width of the modal container.\n            // Otherwise, images will have some \"margin\" on the left and right.\n            val newHeight = min(measuredHeight, maxHeight) + 1\n            setMeasuredDimension(newWidth, newHeight)\n        } else {\n            setMeasuredDimension(measuredWidth, measuredHeight)\n        }\n        if (setToHalfParentHeight) {\n            val parentHeight = (parent as View).height\n            setMeasuredDimension(measuredWidth, (parentHeight * 0.5).toInt())\n        }\n    }\n\n    /**\n     * Clips the input canvas to a rounded rectangle of the specified width and height, using the\n     * radii set in [setCornersRadiiPx].\n     *\n     * @param canvas the canvas to be clipped\n     * @param widthPx\n     * @param heightPx\n     * @return whether the canvas was successfully clipped\n     */\n    fun clipCanvasToPath(canvas: Canvas, widthPx: Int, heightPx: Int): Boolean {\n        if (!this::inAppRadii.isInitialized) {\n            brazelog { \"In-app message radii is uninitialized, not clipping path.\" }\n            return false\n        }\n        return try {\n            clipPath.reset()\n            rectf[0.0f, 0.0f, widthPx.toFloat()] = heightPx.toFloat()\n            clipPath.addRoundRect(rectf, inAppRadii, Path.Direction.CW)\n            canvas.clipPath(clipPath)\n            true\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Encountered exception while trying to clip in-app message image\" }\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageImmersiveBaseView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.graphics.Rect\nimport android.os.Build\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.TouchDelegate\nimport android.view.View\nimport android.widget.TextView\nimport com.braze.ui.R\nimport com.braze.enums.inappmessage.TextAlign\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.utils.InAppMessageButtonViewUtils.setButtons\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.closeInAppMessageOnKeycodeBack\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.resetMessageMarginsIfNecessary\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setFrameColor\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setTextAlignment\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setTextViewColor\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColorFilter\nimport com.braze.ui.support.removeViewFromParent\n\n@Suppress(\"TooManyFunctions\")\nabstract class InAppMessageImmersiveBaseView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageBaseView(context, attrs), IInAppMessageImmersiveView {\n    abstract val frameView: View?\n    abstract override val messageTextView: TextView?\n    abstract val messageHeaderTextView: TextView?\n\n    override fun resetMessageMargins(imageRetrievalSuccessful: Boolean) {\n        super.resetMessageMargins(imageRetrievalSuccessful)\n        if (messageTextView?.text.toString().isBlank()) {\n            messageTextView.removeViewFromParent()\n        }\n        if (messageHeaderTextView?.text.toString().isBlank()) {\n            messageHeaderTextView.removeViewFromParent()\n        }\n        resetMessageMarginsIfNecessary(messageTextView, messageHeaderTextView)\n    }\n\n    @Suppress(\"LongMethod\")\n    override fun setupDirectionalNavigation(numButtons: Int) {\n        // Buttons should focus to each other and the close button\n        val messageButtonViews = getMessageButtonViews(numButtons)\n        val closeButton = messageCloseButtonView\n        val closeButtonId = closeButton?.id\n        // If the user happens to leave touch mode while the IAM is already on-screen,\n        // we need to specify what View will receive that initial focus.\n        var defaultFocusId = closeButtonId\n        var defaultFocusView: View? = closeButton\n        val primaryButton: View?\n        val secondaryButton: View?\n        val primaryId: Int?\n        val secondaryId: Int?\n        if (closeButtonId == null) {\n            brazelog(W) { \"closeButtonId is null. Cannot continue setting up navigation.\" }\n            return\n        }\n        when (numButtons) {\n            2 -> {\n                primaryButton = messageButtonViews[1]\n                secondaryButton = messageButtonViews[0]\n                primaryId = primaryButton.id\n                secondaryId = secondaryButton.id\n                defaultFocusId = primaryId\n                defaultFocusView = primaryButton\n\n                // Primary points to close and secondary button\n                primaryButton.nextFocusLeftId = secondaryId\n                primaryButton.nextFocusRightId = secondaryId\n                primaryButton.nextFocusUpId = closeButtonId\n                primaryButton.nextFocusDownId = closeButtonId\n\n                // Secondary also points to close and secondary button\n                secondaryButton.nextFocusLeftId = primaryId\n                secondaryButton.nextFocusRightId = primaryId\n                secondaryButton.nextFocusUpId = closeButtonId\n                secondaryButton.nextFocusDownId = closeButtonId\n\n                // Close button points to primary, then secondary\n                closeButton.nextFocusUpId = primaryId\n                closeButton.nextFocusDownId = primaryId\n                closeButton.nextFocusRightId = primaryId\n                closeButton.nextFocusLeftId = secondaryId\n            }\n            1 -> {\n                primaryButton = messageButtonViews[0]\n                primaryId = primaryButton.id\n                defaultFocusId = primaryId\n                defaultFocusView = primaryButton\n\n                // Primary points to close\n                primaryButton.nextFocusLeftId = closeButtonId\n                primaryButton.nextFocusRightId = closeButtonId\n                primaryButton.nextFocusUpId = closeButtonId\n                primaryButton.nextFocusDownId = closeButtonId\n\n                // Close button points to primary\n                closeButton.nextFocusUpId = primaryId\n                closeButton.nextFocusDownId = primaryId\n                closeButton.nextFocusRightId = primaryId\n                closeButton.nextFocusLeftId = primaryId\n            }\n            0 -> {\n                // Have the close button wrap back to itself\n                closeButton.nextFocusUpId = closeButtonId\n                closeButton.nextFocusDownId = closeButtonId\n                closeButton.nextFocusRightId = closeButtonId\n                closeButton.nextFocusLeftId = closeButtonId\n            }\n            else -> brazelog(W) {\n                \"Cannot setup directional navigation. Got unsupported number of buttons: $numButtons\"\n            }\n        }\n\n        // The entire view should focus back to the close\n        // button and not allow for backwards navigation.\n        if (defaultFocusId != null) {\n            this.nextFocusUpId = defaultFocusId\n            this.nextFocusDownId = defaultFocusId\n            this.nextFocusRightId = defaultFocusId\n            this.nextFocusLeftId = defaultFocusId\n        }\n\n        // Request focus for the default view\n        val finalDefaultFocusView = defaultFocusView\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            finalDefaultFocusView?.isFocusedByDefault = true\n        }\n        finalDefaultFocusView?.post { finalDefaultFocusView.requestFocus() }\n    }\n\n    open fun setMessageButtons(messageButtons: List<MessageButton>) {\n        setButtons(getMessageButtonViews(messageButtons.size), messageButtons)\n    }\n\n    open fun setMessageCloseButtonColor(color: Int) {\n        messageCloseButtonView?.let { setViewBackgroundColorFilter(it, color) }\n    }\n\n    open fun setMessageHeaderTextColor(color: Int) {\n        messageHeaderTextView?.let {\n            setTextViewColor(it, color)\n        }\n    }\n\n    open fun setMessageHeaderText(text: String) {\n        messageHeaderTextView?.text = text\n    }\n\n    open fun setMessageHeaderTextAlignment(textAlign: TextAlign) {\n        messageHeaderTextView?.let {\n            setTextAlignment(it, textAlign)\n        }\n    }\n\n    open fun setFrameColor(color: Int) {\n        frameView?.let { setFrameColor(it, color) }\n    }\n\n    /**\n     * Returns a list of all button views for this [IInAppMessageImmersiveView]. The default views\n     * for in-app messages can contain multiple layouts depending on the number of buttons.\n     *\n     * @param numButtons The number of buttons used for this layout.\n     */\n    abstract override fun getMessageButtonViews(numButtons: Int): List<View>\n\n    /**\n     * Immersive messages can alternatively be closed by the back button.\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        if (keyCode == KeyEvent.KEYCODE_BACK && BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        return super.onKeyDown(keyCode, event)\n    }\n\n    /**\n     * Immersive messages can alternatively be closed by the back button.\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun dispatchKeyEvent(event: KeyEvent): Boolean {\n        if (!isInTouchMode && event.keyCode == KeyEvent.KEYCODE_BACK && BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        return super.dispatchKeyEvent(event)\n    }\n\n    /**\n     * Sets a rectangular click area for the close button. This is necessary to provide a larger click\n     * area than the close button drawable and to ensure that the click area is not a mask of the drawable\n     * and is instead an easy to tap rectangle.\n     *\n     * @param closeButtonView The close button view.\n     */\n    open fun setLargerCloseButtonClickArea(closeButtonView: View?) {\n        if (closeButtonView == null || closeButtonView.parent == null) {\n            brazelog(W) { \"Cannot increase click area for view if view and/or parent are null.\" }\n            return\n        }\n        val parent = closeButtonView.parent\n        if (parent is View) {\n            parent.post {\n                val delegateArea = Rect()\n\n                // The hit rectangle for the ImageButton\n                closeButtonView.getHitRect(delegateArea)\n\n                // Extend the touch area of the ImageButton beyond its bounds\n                val desiredCloseButtonClickAreaWidth =\n                    context.resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_close_button_click_area_width)\n                val desiredCloseButtonClickAreaHeight =\n                    context.resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_close_button_click_area_height)\n                val extraHorizontalPadding = (desiredCloseButtonClickAreaWidth - delegateArea.width()) / 2\n                val extraVerticalPadding = (desiredCloseButtonClickAreaHeight - delegateArea.height()) / 2\n                delegateArea.top -= extraVerticalPadding\n                delegateArea.bottom += extraVerticalPadding\n                delegateArea.left -= extraHorizontalPadding\n                delegateArea.right += extraHorizontalPadding\n\n                // Instantiate a TouchDelegate.\n                // \"delegateArea\" is the bounds in local coordinates of\n                // the containing view to be mapped to the delegate view.\n                val touchDelegate = TouchDelegate(delegateArea, closeButtonView)\n\n                // Sets the TouchDelegate on the parent view, such that touches\n                // within the touch delegate bounds are routed to the child.\n                parent.touchDelegate = touchDelegate\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageModalView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport com.braze.ui.R\nimport com.braze.enums.inappmessage.ImageStyle\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.models.inappmessage.InAppMessageModal\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams.modalizedImageRadiusDp\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColorFilter\nimport com.braze.ui.support.convertDpToPixels\nimport com.braze.support.BrazeLogger.brazelog\nimport kotlin.math.min\n\nopen class InAppMessageModalView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageImmersiveBaseView(context, attrs) {\n\n    protected var inAppMessageImageView: InAppMessageImageView? = null\n    protected var inAppMessage: InAppMessageModal? = null\n\n    override val frameView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_modal_frame)\n    override val messageTextView: TextView?\n        get() = findViewById(R.id.com_braze_inappmessage_modal_message)\n    override val messageHeaderTextView: TextView?\n        get() = findViewById(R.id.com_braze_inappmessage_modal_header_text)\n    override val messageClickableView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_modal)\n    override val messageCloseButtonView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_modal_close_button)\n    override val messageIconView: TextView?\n        get() = findViewById(R.id.com_braze_inappmessage_modal_icon)\n    override val messageBackgroundObject: Drawable?\n        get() = messageClickableView?.background\n    override val messageImageView: ImageView?\n        get() = inAppMessageImageView\n\n    override fun resetMessageMargins(imageRetrievalSuccessful: Boolean) {\n        super.resetMessageMargins(imageRetrievalSuccessful)\n        // If the in-app message contains an image or icon, reset the image layout's margins to 0.\n        // When there is no image or icon present, the layout has a top margin of 20 to create 20dp\n        // of padding between the text content and the top of the message.\n        val imageLayout =\n            findViewById<RelativeLayout>(R.id.com_braze_inappmessage_modal_image_layout)\n        if (imageRetrievalSuccessful || messageIconView != null) {\n            if (imageLayout != null) {\n                val layoutParams =\n                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)\n                layoutParams.setMargins(0, 0, 0, 0)\n                imageLayout.layoutParams = layoutParams\n            }\n        }\n\n        // Make scrollView pass click events to message clickable view, so that clicking on the scrollView\n        // dismisses the in-app message.\n        val scrollViewChild = findViewById<View>(R.id.com_braze_inappmessage_modal_text_layout)\n        scrollViewChild?.setOnClickListener {\n            brazelog { \"Passing scrollView click event to message clickable view.\" }\n            messageClickableView?.performClick()\n        }\n    }\n\n    override fun setMessageBackgroundColor(color: Int) {\n        setViewBackgroundColorFilter(findViewById(R.id.com_braze_inappmessage_modal), color)\n    }\n\n    override fun getMessageButtonViews(numButtons: Int): List<View> {\n        val buttonViews = mutableListOf<View>()\n\n        // Based on the number of buttons, make one of the button parent layouts visible\n        if (numButtons == 1) {\n            val singleButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_modal_button_layout_single)\n            singleButtonParent?.visibility = VISIBLE\n            val singleButton =\n                findViewById<View>(R.id.com_braze_inappmessage_modal_button_single_one)\n            if (singleButton != null) {\n                buttonViews.add(singleButton)\n            }\n        } else if (numButtons == 2) {\n            val dualButtonParent =\n                findViewById<View>(R.id.com_braze_inappmessage_modal_button_layout_dual)\n            dualButtonParent?.visibility = VISIBLE\n            val dualButton1 = findViewById<View>(R.id.com_braze_inappmessage_modal_button_dual_one)\n            val dualButton2 = findViewById<View>(R.id.com_braze_inappmessage_modal_button_dual_two)\n            if (dualButton1 != null) {\n                buttonViews.add(dualButton1)\n            }\n            if (dualButton2 != null) {\n                buttonViews.add(dualButton2)\n            }\n        }\n        return buttonViews\n    }\n\n    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {\n        super.onLayout(changed, left, top, right, bottom)\n        resizeGraphicFrameIfAppropriate(this.context, inAppMessage)\n    }\n\n    open fun applyInAppMessageParameters(context: Context, inAppMessage: InAppMessageModal) {\n        this.inAppMessage = inAppMessage\n        inAppMessageImageView = findViewById(R.id.com_braze_inappmessage_modal_imageview)\n        inAppMessageImageView?.let { setInAppMessageImageViewAttributes(context, inAppMessage, it) }\n        resizeGraphicFrameIfAppropriate(context, inAppMessage)\n    }\n\n    /**\n     * Programmatically set attributes on the image view classes inside the image ViewStubs.\n     */\n    protected open fun setInAppMessageImageViewAttributes(\n        context: Context,\n        inAppMessage: IInAppMessageImmersive,\n        inAppMessageImageView: IInAppMessageImageView\n    ) {\n        val pixelRadius = convertDpToPixels(context, modalizedImageRadiusDp).toFloat()\n        if (inAppMessage.imageStyle == ImageStyle.GRAPHIC) {\n            inAppMessageImageView.setCornersRadiusPx(pixelRadius)\n        } else {\n            inAppMessageImageView.setCornersRadiiPx(pixelRadius, pixelRadius, 0.0f, 0.0f)\n        }\n        inAppMessageImageView.setInAppMessageImageCropType(inAppMessage.cropType)\n    }\n\n    /**\n     * If displaying a graphic modal, resize its bounds based on the aspect ratio of the input image\n     * and its maximum size.\n     */\n    protected open fun resizeGraphicFrameIfAppropriate(\n        context: Context,\n        inAppMessage: InAppMessageModal?\n    ) {\n        val bitmap = inAppMessage?.bitmap ?: return\n        if (inAppMessage.imageStyle != ImageStyle.GRAPHIC) {\n            return\n        }\n        val imageAspectRatio = bitmap.width.toDouble() / bitmap.height\n        val resources = context.resources\n        val marginPixels =\n            resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_modal_margin)\n        val maxModalWidth =\n            resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_modal_max_width)\n        val maxModalHeight =\n            resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_modal_max_height)\n\n        // The measured width is only available after the draw phase, which\n        // this runnable will draw after.\n        post {\n            val maxWidthPixelSize = min(measuredWidth - marginPixels, maxModalWidth).toDouble()\n            val maxHeightPixelSize =\n                min(measuredHeight - marginPixels, maxModalHeight).toDouble()\n            val maxSizeAspectRatio = maxWidthPixelSize / maxHeightPixelSize\n            val modalBoundView = findViewById<View>(R.id.com_braze_inappmessage_modal_graphic_bound)\n            if (modalBoundView != null) {\n                val params = modalBoundView.layoutParams as LayoutParams\n                if (imageAspectRatio >= maxSizeAspectRatio) {\n                    params.width = maxWidthPixelSize.toInt()\n                    params.height = (maxWidthPixelSize / imageAspectRatio).toInt()\n                } else {\n                    params.width = (maxHeightPixelSize * imageAspectRatio).toInt()\n                    params.height = maxHeightPixelSize.toInt()\n                }\n                modalBoundView.layoutParams = params\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageSlideupView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.graphics.drawable.GradientDrawable\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport androidx.core.view.WindowInsetsCompat\nimport com.braze.ui.R\nimport com.braze.enums.inappmessage.ClickAction\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.setViewBackgroundColorFilter\nimport com.braze.ui.support.getMaxSafeBottomInset\nimport com.braze.ui.support.getMaxSafeLeftInset\nimport com.braze.ui.support.getMaxSafeRightInset\nimport com.braze.ui.support.getMaxSafeTopInset\nimport com.braze.ui.support.removeViewFromParent\n\nopen class InAppMessageSlideupView(context: Context?, attrs: AttributeSet?) :\n    InAppMessageBaseView(context, attrs) {\n    private var inAppMessageImageView: InAppMessageImageView? = null\n\n    override val messageTextView: TextView?\n        get() = findViewById(R.id.com_braze_inappmessage_slideup_message)\n    override val messageBackgroundObject: View?\n        get() = findViewById(R.id.com_braze_inappmessage_slideup_container)\n    override val messageImageView: ImageView?\n        get() = inAppMessageImageView\n    override val messageIconView: TextView?\n        get() = findViewById(R.id.com_braze_inappmessage_slideup_icon)\n    private val messageChevronView: View?\n        get() = findViewById(R.id.com_braze_inappmessage_slideup_chevron)\n\n    fun applyInAppMessageParameters(inAppMessage: IInAppMessage) {\n        inAppMessageImageView = findViewById(R.id.com_braze_inappmessage_slideup_imageview)\n        inAppMessageImageView?.setInAppMessageImageCropType(inAppMessage.cropType)\n    }\n\n    fun setMessageChevron(color: Int, clickAction: ClickAction) {\n        if (clickAction == ClickAction.NONE) {\n            messageChevronView?.visibility = GONE\n        } else {\n            messageChevronView?.let { setViewBackgroundColorFilter(it, color) }\n        }\n    }\n\n    override fun setMessageBackgroundColor(color: Int) {\n        if (messageBackgroundObject?.background is GradientDrawable) {\n            messageBackgroundObject?.let { setViewBackgroundColorFilter(it, color) }\n        } else {\n            super.setMessageBackgroundColor(color)\n        }\n    }\n\n    override fun resetMessageMargins(imageRetrievalSuccessful: Boolean) {\n        super.resetMessageMargins(imageRetrievalSuccessful)\n        val isIconBlank = messageIconView?.text?.isEmpty() != false\n\n        if (!imageRetrievalSuccessful && isIconBlank) {\n            // There's neither an icon nor an image\n            // Remove the image container layout and reset our text's left margin\n            val imageContainerLayout =\n                findViewById<RelativeLayout>(R.id.com_braze_inappmessage_slideup_image_layout)\n            imageContainerLayout?.removeViewFromParent()\n\n            // Reset the margin for the message\n            val slideupMessage = findViewById<TextView>(R.id.com_braze_inappmessage_slideup_message)\n            val layoutParams = slideupMessage?.layoutParams as LayoutParams?\n            layoutParams?.leftMargin =\n                context.resources.getDimensionPixelSize(R.dimen.com_braze_inappmessage_slideup_left_message_margin_no_image)\n            slideupMessage.layoutParams = layoutParams\n        }\n    }\n\n    /**\n     * Applies the [WindowInsetsCompat] by ensuring any part of the slideup does not render in the cutout area.\n     *\n     * @param insets The [WindowInsetsCompat] object directly from\n     * [androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener].\n     */\n    override fun applyWindowInsets(insets: WindowInsetsCompat) {\n        super.applyWindowInsets(insets)\n        if (layoutParams == null || layoutParams !is MarginLayoutParams) {\n            brazelog { \"Close button view is null or not of the expected class. Not applying window insets.\" }\n            return\n        }\n\n        // Offset the existing margin with whatever the inset margins safe area values are\n        val layoutParams = layoutParams as MarginLayoutParams\n        layoutParams.setMargins(\n            getMaxSafeLeftInset(insets) + layoutParams.leftMargin,\n            getMaxSafeTopInset(insets) + layoutParams.topMargin,\n            getMaxSafeRightInset(insets) + layoutParams.rightMargin,\n            getMaxSafeBottomInset(insets) + layoutParams.bottomMargin\n        )\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/inappmessage/views/InAppMessageWebView.kt",
    "content": "package com.braze.ui.inappmessage.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.webkit.WebView\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils.closeInAppMessageOnKeycodeBack\n\n/**\n * WebView embedded in Braze html in-app messages.\n */\nopen class InAppMessageWebView(context: Context, attrs: AttributeSet?) : WebView(\n    context, attrs\n) {\n    /**\n     * If the back button is pressed while this WebView is in focus,\n     * close the current in-app message.\n     *\n     * Note: When this WebView doesn't have focus, back button events on html in-app messages are\n     * captured by [InAppMessageHtmlFullView.onKeyDown]\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        if (keyCode == KeyEvent.KEYCODE_BACK &&\n            BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView\n        ) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        return super.onKeyDown(keyCode, event)\n    }\n\n    /**\n     * WebView-based messages can alternatively be closed by the back button.\n     *\n     * @return If the button pressed was the back button, close the in-app message\n     * and return true to indicate that the event was handled.\n     */\n    override fun dispatchKeyEvent(event: KeyEvent): Boolean {\n        if (!isInTouchMode && event.keyCode == KeyEvent.KEYCODE_BACK &&\n            BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView\n        ) {\n            closeInAppMessageOnKeycodeBack()\n            return true\n        }\n        return super.dispatchKeyEvent(event)\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/support/AnimationUtils.kt",
    "content": "@file:JvmName(\"AnimationUtils\")\n\npackage com.braze.ui.support\n\nimport android.view.animation.AccelerateInterpolator\nimport android.view.animation.Animation\nimport android.view.animation.DecelerateInterpolator\nimport android.view.animation.Interpolator\nimport android.view.animation.TranslateAnimation\n\nprivate val accelerateInterpolator: Interpolator = AccelerateInterpolator()\nprivate val decelerateInterpolator: Interpolator = DecelerateInterpolator()\n\n/**\n * @param fromY Change in Y coordinate to apply at the start of the animation, represented as a percentage (where 1.0 is 100%).\n * @param toY Change in Y coordinate to apply at the end of the animation, represented as a percentage (where 1.0 is 100%).\n * @param duration Amount of time (in milliseconds) for the animation to run.\n * @param accelerate Whether to use the accelerate interpolator or the decelerate interpolator.\n * @return an Animation object with appropriate vertical transformation and duration\n */\nfun createVerticalAnimation(\n    fromY: Float,\n    toY: Float,\n    duration: Long,\n    accelerate: Boolean\n): Animation {\n    val animation = TranslateAnimation(\n        Animation.RELATIVE_TO_PARENT, 0.0f,\n        Animation.RELATIVE_TO_PARENT, 0.0f,\n        Animation.RELATIVE_TO_SELF, fromY,\n        Animation.RELATIVE_TO_SELF, toY\n    )\n    return setAnimationParams(animation, duration, accelerate)\n}\n\n/**\n * @param fromX Change in X coordinate to apply at the start of the animation, represented as a percentage (where 1.0 is 100%).\n * @param toX Change in X coordinate to apply at the end of the animation, represented as a percentage (where 1.0 is 100%).\n * @param duration Amount of time (in milliseconds) for the animation to run.\n * @param accelerate Whether to use the accelerate interpolator or the decelerate interpolator.\n * @return an Animation object with appropriate horizontal transformation and duration\n */\nfun createHorizontalAnimation(\n    fromX: Float,\n    toX: Float,\n    duration: Long,\n    accelerate: Boolean\n): Animation {\n    val animation = TranslateAnimation(\n        Animation.RELATIVE_TO_SELF, fromX,\n        Animation.RELATIVE_TO_SELF, toX,\n        Animation.RELATIVE_TO_PARENT, 0.0f,\n        Animation.RELATIVE_TO_PARENT, 0.0f\n    )\n    return setAnimationParams(animation, duration, accelerate)\n}\n\n/**\n * Sets duration and interpolator for the given Animation object.\n *\n * @param animation The Animation object to modify.\n * @param duration Amount of time (in milliseconds) for the animation to run.\n * @param accelerate Whether to use the accelerate interpolator or the decelerate interpolator.\n * @return the input Animation with duration and interpolator set\n */\nfun setAnimationParams(animation: Animation, duration: Long, accelerate: Boolean): Animation {\n    animation.duration = duration\n    if (accelerate) {\n        animation.interpolator = accelerateInterpolator\n    } else {\n        animation.interpolator = decelerateInterpolator\n    }\n    return animation\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/support/UriUtils.kt",
    "content": "@file:JvmName(\"UriUtils\")\n\npackage com.braze.ui.support\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport com.braze.IBrazeDeeplinkHandler\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport com.braze.ui.BrazeDeeplinkHandler\n\nprivate val TAG = \"UriUtils\".getBrazeLogTag()\n\n/**\n * Parses the query part of the uri and returns a mapping of the query keys to the\n * values. Empty keys or empty values will not be included in the mapping.\n */\nfun Uri.getQueryParameters(): Map<String, String> {\n    var uri = this\n    val encodedQuery = uri.encodedQuery\n    if (encodedQuery == null) {\n        brazelog(TAG, V) { \"Encoded query is null for Uri: $uri Returning empty map for query parameters\" }\n        return emptyMap()\n    }\n    val parameterValues = mutableMapOf<String, String>()\n    try {\n        if (uri.isOpaque) {\n            // Convert the opaque uri into a parseable hierarchical one\n            // This is basically copying the query from the original uri onto a new one\n            uri = Uri.parse(\"://\")\n                .buildUpon()\n                .encodedQuery(encodedQuery)\n                .build()\n        }\n        val queryParameterNames = uri.queryParameterNames.filter { !it.isNullOrEmpty() }\n        for (queryParameterKey in queryParameterNames) {\n            val queryParameterValue = uri.getQueryParameter(queryParameterKey)\n            if (!queryParameterValue.isNullOrEmpty()) {\n                parameterValues[queryParameterKey] = queryParameterValue\n            }\n        }\n    } catch (e: Exception) {\n        brazelog(TAG, E, e) { \"Failed to map the query parameters of Uri: $uri\" }\n    }\n    return parameterValues\n}\n\nfun getMainActivityIntent(context: Context, extras: Bundle? = null): Intent? {\n    val startActivityIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)\n    startActivityIntent?.flags = BrazeDeeplinkHandler.getInstance()\n        .getIntentFlags(IBrazeDeeplinkHandler.IntentFlagPurpose.URI_UTILS_GET_MAIN_ACTIVITY_INTENT)\n    if (extras != null) {\n        startActivityIntent?.putExtras(extras)\n    }\n    return startActivityIntent\n}\n\n/**\n * @param context The context used to create the checked component identifier.\n * @param className The class name for a registered activity with the given context\n * @return true if the class name matches a registered activity in the Android Manifest.\n */\nfun isActivityRegisteredInManifest(context: Context, className: String): Boolean {\n    return try {\n        // If the activity is registered, then a non-null ActivityInfo is returned by the package manager.\n        // If unregistered, then an exception is thrown by the package manager.\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            context.packageManager.getActivityInfo(ComponentName(context, className), PackageManager.ComponentInfoFlags.of(0))\n        } else {\n            @Suppress(\"DEPRECATION\")\n            context.packageManager.getActivityInfo(ComponentName(context, className), 0)\n        }\n        true\n    } catch (e: PackageManager.NameNotFoundException) {\n        brazelog(TAG, W, e) { \"Could not find activity info for class with name: $className\" }\n        false\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/support/ViewUtils.kt",
    "content": "@file:JvmName(\"ViewUtils\")\n@file:Suppress(\"TooManyFunctions\")\n\npackage com.braze.ui.support\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.WindowInsetsCompat\nimport com.braze.enums.inappmessage.Orientation\nimport com.braze.support.BrazeLogger.Priority.D\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport kotlin.math.max\n\nprivate val TAG = \"ViewUtils\".getBrazeLogTag()\nprivate const val TABLET_SMALLEST_WIDTH_DP = 600\n\nfun View?.removeViewFromParent() {\n    if (this == null) {\n        brazelog(TAG, D) { \"View passed in is null. Not removing from parent.\" }\n    }\n    if (this?.parent is ViewGroup) {\n        val parent = this.parent as ViewGroup\n        parent.removeView(this)\n        brazelog(TAG, D) { \"Removed view: $this\\nfrom parent: $parent\" }\n    }\n}\n\nfun View.setFocusableInTouchModeAndRequestFocus() {\n    try {\n        this.isFocusableInTouchMode = true\n        this.requestFocus()\n    } catch (e: Exception) {\n        brazelog(TAG, E, e) {\n            \"Caught exception while setting view to focusable in touch mode and requesting focus.\"\n        }\n    }\n}\n\nfun convertDpToPixels(context: Context, valueInDp: Double): Double {\n    val density = context.resources.displayMetrics.density.toDouble()\n    return valueInDp * density\n}\n\nfun Activity.isRunningOnTablet(): Boolean {\n    return (\n        this.resources.configuration.smallestScreenWidthDp\n            >= TABLET_SMALLEST_WIDTH_DP\n        )\n}\n\n/**\n * Safely calls [Activity.setRequestedOrientation].\n */\nfun Activity.setActivityRequestedOrientation(requestedOrientation: Int) {\n    try {\n        this.requestedOrientation = requestedOrientation\n    } catch (e: Exception) {\n        brazelog(TAG, E, e) {\n            \"Failed to set requested orientation $requestedOrientation for activity class: ${this.localClassName}\"\n        }\n    }\n}\n\nfun setHeightOnViewLayoutParams(view: View, height: Int) {\n    val layoutParams = view.layoutParams\n    layoutParams.height = height\n    view.layoutParams = layoutParams\n}\n\n/**\n * Checks if the device is in night mode. In Android 10+, this corresponds\n * to \"Dark Theme\" being enabled by the user.\n */\nfun isDeviceInNightMode(context: Context): Boolean {\n    val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK\n    return nightModeFlags == Configuration.UI_MODE_NIGHT_YES\n}\n\n/**\n * @return Whether the current screen orientation (e.g. [Configuration.ORIENTATION_LANDSCAPE])\n * matches the preferred orientation (e.g. [Orientation.LANDSCAPE].\n */\nfun isCurrentOrientationValid(\n    currentScreenOrientation: Int,\n    preferredOrientation: Orientation\n): Boolean {\n    return if (currentScreenOrientation == Configuration.ORIENTATION_LANDSCAPE\n        && preferredOrientation === Orientation.LANDSCAPE\n    ) {\n        brazelog(TAG, D) { \"Current and preferred orientation are landscape.\" }\n        true\n    } else if (currentScreenOrientation == Configuration.ORIENTATION_PORTRAIT\n        && preferredOrientation === Orientation.PORTRAIT\n    ) {\n        brazelog(TAG, D) { \"Current and preferred orientation are portrait.\" }\n        true\n    } else {\n        brazelog(TAG, D) {\n            \"Current orientation $currentScreenOrientation\" +\n                \" and preferred orientation $preferredOrientation don't match\"\n        }\n        false\n    }\n}\n\n/**\n * @return The maximum of the display cutout left inset and the system window left inset.\n */\nfun getMaxSafeLeftInset(windowInsets: WindowInsetsCompat): Int {\n    return max(\n        windowInsets.displayCutout?.safeInsetLeft ?: 0,\n        windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).left\n    )\n}\n\n/**\n * @return The maximum of the display cutout right inset and the system window right inset.\n */\nfun getMaxSafeRightInset(windowInsets: WindowInsetsCompat): Int {\n    return max(\n        windowInsets.displayCutout?.safeInsetRight ?: 0,\n        windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).right\n    )\n}\n\n/**\n * @return The maximum of the display cutout top inset and the system window top inset.\n */\nfun getMaxSafeTopInset(windowInsets: WindowInsetsCompat): Int {\n    return max(\n        windowInsets.displayCutout?.safeInsetTop ?: 0,\n        windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top\n    )\n}\n\n/**\n * @return The maximum of the display cutout bottom inset and the system window bottom inset.\n */\nfun getMaxSafeBottomInset(windowInsets: WindowInsetsCompat): Int {\n    return max(\n        windowInsets.displayCutout?.safeInsetBottom ?: 0,\n        windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom\n    )\n}\n\n/**\n * Detects if this device is currently in touch mode given a [View].\n */\nfun isDeviceNotInTouchMode(view: View) =\n    !view.isInTouchMode\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/BannerImageCardView.java",
    "content": "package com.braze.ui.widget;\n\nimport android.content.Context;\nimport android.widget.ImageView;\n\nimport com.braze.models.cards.BannerImageCard;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.actions.IAction;\n\npublic class BannerImageCardView extends BaseFeedCardView<BannerImageCard> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BannerImageCardView.class);\n  private final ImageView mImage;\n  private IAction mCardAction;\n\n  // We set this card's aspect ratio here as a first guess. If the server doesn't send down an\n  // aspect ratio, then this value will be the aspect ratio of the card on render.\n  private float mAspectRatio = 6f;\n\n  public BannerImageCardView(Context context) {\n    this(context, null);\n  }\n\n  @SuppressWarnings(\"deprecation\") // getDrawable() until Build.VERSION_CODES.LOLLIPOP\n  public BannerImageCardView(final Context context, BannerImageCard card) {\n    super(context);\n    mImage = (ImageView) getProperViewFromInflatedStub(R.id.com_braze_banner_image_card_imageview_stub);\n    mImage.setScaleType(ImageView.ScaleType.CENTER_CROP);\n    mImage.setAdjustViewBounds(true);\n\n    if (card != null) {\n      setCard(card);\n    }\n\n    setBackground(getResources().getDrawable(R.drawable.com_braze_card_background));\n  }\n\n  @Override\n  protected int getLayoutResource() {\n    return R.layout.com_braze_banner_image_card;\n  }\n\n  @Override\n  public void onSetCard(final BannerImageCard card) {\n    if (card.getAspectRatio() != 0f) {\n      mAspectRatio = card.getAspectRatio();\n    }\n    setImageViewToUrl(mImage, card.getImageUrl(), mAspectRatio, card);\n\n    mCardAction = getUriActionForCard(card);\n    setOnClickListener(view -> handleCardClick(applicationContext, card, mCardAction));\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/BaseCardView.kt",
    "content": "package com.braze.ui.widget\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.ViewTreeObserver\nimport android.widget.ImageView\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport com.braze.models.cards.Card\nimport com.braze.ui.R\nimport com.braze.ui.feed.BrazeImageSwitcher\nimport com.braze.Braze\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.getInstance\nimport com.braze.ui.actions.IAction\nimport com.braze.ui.actions.UriAction\n\n/**\n * Base class for Braze feed card views.\n */\nabstract class BaseCardView<T : Card>(context: Context) : RelativeLayout(context) {\n    @JvmField\n    protected val applicationContext: Context = context.applicationContext\n    val classLogTag: String = getBrazeLogTag(this.javaClass)\n    @JvmField\n    protected var card: T? = null\n    @JvmField\n    var imageSwitcher: BrazeImageSwitcher? = null\n    @JvmField\n    protected var configurationProvider = BrazeConfigurationProvider(context)\n\n    private val isUnreadCardVisualIndicatorEnabled: Boolean = configurationProvider.isNewsfeedVisualIndicatorOn\n\n    val isUnreadIndicatorEnabled: Boolean\n        get() = isUnreadCardVisualIndicatorEnabled\n\n    /**\n     * Applies the text to the [TextView]. If the text is null or blank,\n     * the [TextView]'s visibility is changed to [android.view.View.GONE].\n     */\n    fun setOptionalTextView(view: TextView, text: String?) {\n        if (!text.isNullOrBlank()) {\n            view.text = text\n            view.visibility = VISIBLE\n        } else {\n            view.text = \"\"\n            view.visibility = GONE\n        }\n    }\n\n    /**\n     * Asynchronously fetches the image at the given imageUrl and displays the image in the ImageView. No image will be\n     * displayed if the image cannot be downloaded or fetched from the cache.\n     *\n     * @param imageView the ImageView in which to display the image\n     * @param imageUrl the URL of the image resource\n     * @param placeholderAspectRatio a placeholder aspect ratio that will be used for sizing purposes.\n     * The actual dimensions of the final image will dictate the final image aspect ratio.\n     * @param card\n     */\n    fun setImageViewToUrl(\n        imageView: ImageView,\n        imageUrl: String,\n        placeholderAspectRatio: Float,\n        card: Card\n    ) {\n        if (imageUrl != imageView.getTag(R.string.com_braze_image_resize_tag_key)) {\n            // If the campaign is using liquid, the aspect ratio could be unknown (0)\n            if (placeholderAspectRatio != 0f) {\n                // We need to set layout params on the imageView once its layout state is visible. To do this,\n                // we obtain the imageView's observer and attach a listener on it for when the view's layout\n                // occurs. At layout time, we set the imageView's size params based on the aspect ratio\n                // for our card. Note that after the card's first layout, we don't want redundant resizing\n                // so we remove our listener after the resizing.\n                val viewTreeObserver = imageView.viewTreeObserver\n                if (viewTreeObserver.isAlive) {\n                    viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {\n                        override fun onPreDraw(): Boolean {\n                            imageView.viewTreeObserver.removeOnPreDrawListener(this)\n                            val width = imageView.width\n                            imageView.layoutParams =\n                                LayoutParams(width, (width / placeholderAspectRatio).toInt())\n                            return true\n                        }\n                    })\n                }\n            }\n            imageView.setImageResource(android.R.color.transparent)\n\n            Braze.getInstance(context).imageLoader.renderUrlIntoCardView(\n                context,\n                card,\n                imageUrl,\n                imageView,\n                BrazeViewBounds.BASE_CARD_VIEW\n            )\n\n            imageView.setTag(R.string.com_braze_image_resize_tag_key, imageUrl)\n        }\n    }\n\n    /**\n     * Checks to see if the card object is viewed and if so, sets the read/unread status\n     * indicator image. If the card is null, does nothing.\n     */\n    fun setCardViewedIndicator(imageSwitcher: BrazeImageSwitcher?, card: Card) {\n        if (imageSwitcher == null) {\n            brazelog(W) { \"imageSwitcher is null. Can't set card viewed indicator.\" }\n            return\n        }\n        // Check the tag for the image switcher so we don't have to re-draw the same indicator unnecessarily\n        var imageSwitcherTag =\n            imageSwitcher.getTag(R.string.com_braze_image_is_read_tag_key)\n        // If the tag is null, default to the empty string\n        imageSwitcherTag = imageSwitcherTag ?: \"\"\n\n        if (card.isIndicatorHighlighted) {\n            if (imageSwitcherTag != ICON_READ_TAG) {\n                if (imageSwitcher.readIcon != null) {\n                    imageSwitcher.setImageDrawable(imageSwitcher.readIcon)\n                } else {\n                    imageSwitcher.setImageResource(R.drawable.com_braze_content_card_icon_read)\n                }\n                imageSwitcher.setTag(R.string.com_braze_image_is_read_tag_key, ICON_READ_TAG)\n            }\n        } else {\n            if (imageSwitcherTag != ICON_UNREAD_TAG) {\n                if (imageSwitcher.unReadIcon != null) {\n                    imageSwitcher.setImageDrawable(imageSwitcher.unReadIcon)\n                } else {\n                    imageSwitcher.setImageResource(R.drawable.com_braze_content_card_icon_unread)\n                }\n                imageSwitcher.setTag(R.string.com_braze_image_is_read_tag_key, ICON_UNREAD_TAG)\n            }\n        }\n    }\n\n    protected fun handleCardClick(context: Context, card: Card, cardAction: IAction?) {\n        brazelog(V) { \"Handling card click for card: $card\" }\n        card.isIndicatorHighlighted = true\n        if (!isClickHandled(context, card, cardAction)) {\n            if (cardAction != null) {\n                card.logClick()\n                brazelog(V) { \"Card action is non-null. Attempting to perform action on card: ${card.id}\" }\n                if (cardAction is UriAction) {\n                    getInstance().gotoUri(context, cardAction)\n                } else {\n                    brazelog { \"Executing non uri action for click on card: ${card.id}\" }\n                    cardAction.execute(context)\n                }\n            } else {\n                brazelog(V) { \"Card action is null. Not performing any click action on card: ${card.id}\" }\n            }\n        } else {\n            brazelog { \"Card click was handled by custom listener on card: ${card.id}\" }\n            card.logClick()\n        }\n    }\n\n    /**\n     * Calls the corresponding card manager to see if the action listener has handled the click.\n     */\n    protected abstract fun isClickHandled(\n        context: Context,\n        card: Card,\n        cardAction: IAction?\n    ): Boolean\n\n    companion object {\n        private const val ICON_READ_TAG = \"icon_read\"\n        private const val ICON_UNREAD_TAG = \"icon_unread\"\n\n        @JvmStatic\n        protected fun getUriActionForCard(card: Card): UriAction? {\n            val extras = Bundle()\n            for (key in card.extras.keys) {\n                extras.putString(key, card.extras[key])\n            }\n            val url = card.url\n            if (url == null) {\n                brazelog(V) { \"Card URL is null, returning null for getUriActionForCard\" }\n                return null\n            }\n            return getInstance().createUriActionFromUrlString(\n                url,\n                extras,\n                card.openUriInWebView,\n                card.channel\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/CaptionedImageCardView.java",
    "content": "package com.braze.ui.widget;\n\nimport android.content.Context;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport com.braze.models.cards.CaptionedImageCard;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.actions.IAction;\n\npublic class CaptionedImageCardView extends BaseFeedCardView<CaptionedImageCard> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(CaptionedImageCardView.class);\n  private final ImageView mImage;\n  private final TextView mTitle;\n  private final TextView mDescription;\n  private final TextView mDomain;\n  private IAction mCardAction;\n\n  // We set this card's aspect ratio here as a first guess. If the server doesn't send down an\n  // aspect ratio, then this value will be the aspect ratio of the card on render.\n  private float mAspectRatio = 4f / 3f;\n\n  public CaptionedImageCardView(Context context) {\n    this(context, null);\n  }\n\n  @SuppressWarnings(\"deprecation\") // getDrawable() until Build.VERSION_CODES.LOLLIPOP\n  public CaptionedImageCardView(final Context context, CaptionedImageCard card) {\n    super(context);\n    mImage = (ImageView) getProperViewFromInflatedStub(R.id.com_braze_captioned_image_card_imageview_stub);\n    mImage.setScaleType(ImageView.ScaleType.CENTER_CROP);\n    mImage.setAdjustViewBounds(true);\n\n    mTitle = findViewById(R.id.com_braze_captioned_image_title);\n    mDescription = findViewById(R.id.com_braze_captioned_image_description);\n    mDomain = findViewById(R.id.com_braze_captioned_image_card_domain);\n\n    if (card != null) {\n      setCard(card);\n    }\n\n    setBackground(getResources().getDrawable(R.drawable.com_braze_card_background));\n  }\n\n  @Override\n  protected int getLayoutResource() {\n    return R.layout.com_braze_captioned_image_card;\n  }\n\n  @Override\n  public void onSetCard(final CaptionedImageCard card) {\n    mTitle.setText(card.getTitle());\n    mDescription.setText(card.getDescription());\n    setOptionalTextView(mDomain, card.getDomain());\n    mCardAction = getUriActionForCard(card);\n    setOnClickListener(view -> handleCardClick(applicationContext, card, mCardAction));\n    mAspectRatio = card.getAspectRatio();\n    setImageViewToUrl(mImage, card.getImageUrl(), mAspectRatio, card);\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/DefaultCardView.java",
    "content": "package com.braze.ui.widget;\n\nimport android.content.Context;\n\nimport com.braze.models.cards.Card;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.support.BrazeLogger;\n\npublic class DefaultCardView extends BaseFeedCardView<Card> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(DefaultCardView.class);\n\n  public DefaultCardView(Context context) {\n    this(context, null);\n  }\n\n  public DefaultCardView(Context context, Card card) {\n    super(context);\n\n    if (card != null) {\n      setCard(card);\n    }\n  }\n\n  @Override\n  protected int getLayoutResource() {\n    return R.layout.com_braze_default_card;\n  }\n\n  @Override public void onSetCard(Card card) {\n    BrazeLogger.w(TAG, \"onSetCard called for blank view with: \" + card.toString());\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/ShortNewsCardView.java",
    "content": "package com.braze.ui.widget;\n\nimport android.content.Context;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport com.braze.models.cards.ShortNewsCard;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.actions.IAction;\n\npublic class ShortNewsCardView extends BaseFeedCardView<ShortNewsCard> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(ShortNewsCardView.class);\n  private final ImageView mImage;\n  private final TextView mTitle;\n  private final TextView mDescription;\n  private final TextView mDomain;\n  private IAction mCardAction;\n  private final float mAspectRatio = 1f;\n\n  public ShortNewsCardView(Context context) {\n    this(context, null);\n  }\n\n  @SuppressWarnings(\"deprecation\") // getDrawable() until Build.VERSION_CODES.LOLLIPOP\n  public ShortNewsCardView(final Context context, ShortNewsCard card) {\n    super(context);\n    mDescription = findViewById(R.id.com_braze_short_news_card_description);\n    mTitle = findViewById(R.id.com_braze_short_news_card_title);\n    mDomain = findViewById(R.id.com_braze_short_news_card_domain);\n\n    mImage = (ImageView) getProperViewFromInflatedStub(R.id.com_braze_short_news_card_imageview_stub);\n    mImage.setScaleType(ImageView.ScaleType.CENTER_CROP);\n    mImage.setAdjustViewBounds(true);\n\n    if (card != null) {\n      setCard(card);\n    }\n\n    setBackground(getResources().getDrawable(R.drawable.com_braze_card_background));\n  }\n\n  @Override\n  protected int getLayoutResource() {\n    return R.layout.com_braze_short_news_card;\n  }\n\n  @Override\n  public void onSetCard(final ShortNewsCard card) {\n    mDescription.setText(card.getDescription());\n    setOptionalTextView(mTitle, card.getTitle());\n    setOptionalTextView(mDomain, card.getDomain());\n    mCardAction = getUriActionForCard(card);\n\n    setOnClickListener(view -> handleCardClick(applicationContext, card, mCardAction));\n\n    setImageViewToUrl(mImage, card.getImageUrl(), mAspectRatio, card);\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/java/com/braze/ui/widget/TextAnnouncementCardView.java",
    "content": "package com.braze.ui.widget;\n\nimport android.content.Context;\nimport android.widget.TextView;\n\nimport com.braze.models.cards.TextAnnouncementCard;\nimport com.braze.ui.R;\nimport com.braze.ui.feed.view.BaseFeedCardView;\nimport com.braze.support.BrazeLogger;\nimport com.braze.ui.actions.IAction;\n\npublic class TextAnnouncementCardView extends BaseFeedCardView<TextAnnouncementCard> {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(TextAnnouncementCardView.class);\n  private final TextView mTitle;\n  private final TextView mDescription;\n  private final TextView mDomain;\n  private IAction mCardAction;\n\n  public TextAnnouncementCardView(Context context) {\n    this(context, null);\n  }\n\n  @SuppressWarnings(\"deprecation\") // getDrawable() until Build.VERSION_CODES.LOLLIPOP\n  public TextAnnouncementCardView(final Context context, TextAnnouncementCard card) {\n    super(context);\n    mTitle = findViewById(R.id.com_braze_text_announcement_card_title);\n    mDescription = findViewById(R.id.com_braze_text_announcement_card_description);\n    mDomain = findViewById(R.id.com_braze_text_announcement_card_domain);\n\n    if (card != null) {\n      setCard(card);\n    }\n\n    setBackground(getResources().getDrawable(R.drawable.com_braze_card_background));\n  }\n\n  @Override\n  protected int getLayoutResource() {\n    return R.layout.com_braze_text_announcement_card;\n  }\n\n  @Override\n  public void onSetCard(final TextAnnouncementCard card) {\n    mTitle.setText(card.getTitle());\n    mDescription.setText(card.getDescription());\n    setOptionalTextView(mDomain, card.getDomain());\n    mCardAction = getUriActionForCard(card);\n\n    setOnClickListener(view -> handleCardClick(applicationContext, card, mCardAction));\n  }\n}\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable/com_braze_inappmessage_close_button_selector.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <item android:drawable=\"@drawable/com_braze_inappmessage_button_close_focused\"\n    android:state_focused=\"true\" />\n  <item android:drawable=\"@drawable/com_braze_inappmessage_button_close\" />\n</selector>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_card_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <!-- Shadow -->\n  <item>\n    <shape>\n      <padding android:bottom=\"@dimen/com_braze_card_background_shadow_bottom\"/>\n      <solid android:color=\"@color/com_braze_card_background_shadow\"/>\n      <corners android:radius=\"@dimen/com_braze_card_background_shadow_radius\"/>\n    </shape>\n  </item>\n  <item>\n    <shape>\n      <padding\n          android:left=\"@dimen/com_braze_card_background_border_left\"\n          android:right=\"@dimen/com_braze_card_background_border_right\"\n          android:top=\"@dimen/com_braze_card_background_border_top\"\n          android:bottom=\"@dimen/com_braze_card_background_border_bottom\"/>\n      <solid android:color=\"@color/com_braze_card_background_border\"/>\n      <corners android:radius=\"@dimen/com_braze_card_background_shadow_radius\"/>\n    </shape>\n  </item>\n\n  <!-- Background -->\n  <item>\n    <shape>\n      <solid android:color=\"@color/com_braze_card_background\"/>\n      <corners android:radius=\"@dimen/com_braze_card_background_corner_radius\"/>\n    </shape>\n  </item>\n</layer-list>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_content_card_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <!-- Shadow -->\n  <item>\n    <shape>\n      <padding android:bottom=\"@dimen/com_braze_content_card_background_shadow_bottom\"/>\n      <solid android:color=\"@color/com_braze_content_card_background_shadow\"/>\n      <corners android:radius=\"@dimen/com_braze_content_card_background_shadow_radius\"/>\n    </shape>\n  </item>\n  <item>\n    <shape>\n      <padding\n          android:left=\"@dimen/com_braze_content_card_background_border_left\"\n          android:right=\"@dimen/com_braze_content_card_background_border_right\"\n          android:top=\"@dimen/com_braze_content_card_background_border_top\"\n          android:bottom=\"@dimen/com_braze_content_card_background_border_bottom\"/>\n      <solid android:color=\"@color/com_braze_content_card_background_border\"/>\n      <corners android:radius=\"@dimen/com_braze_content_card_background_shadow_radius\"/>\n    </shape>\n  </item>\n\n  <!-- Background -->\n  <item>\n    <shape>\n      <solid android:color=\"@color/com_braze_content_card_background\"/>\n      <corners android:radius=\"@dimen/com_braze_content_card_background_corner_radius\"/>\n    </shape>\n  </item>\n</layer-list>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_content_card_scrim.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <item android:state_focused=\"true\"\n      android:drawable=\"@drawable/com_braze_content_card_scrim_focused\" /> <!-- focused -->\n  <item android:state_hovered=\"true\"\n      android:drawable=\"@drawable/com_braze_content_card_scrim_focused\" /> <!-- hovered -->\n</selector>"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_content_card_scrim_focused.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <!-- Background -->\n  <item>\n    <shape>\n      <solid android:color=\"@color/com_braze_content_card_focus_scrim\"/>\n      <corners android:radius=\"@dimen/com_braze_content_card_background_corner_radius\"/>\n    </shape>\n  </item>\n</layer-list>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_content_cards_rounded_corner_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\"rectangle\">\n  <corners android:radius=\"@dimen/com_braze_content_cards_image_border_radius\"/>\n</shape>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_content_cards_unread_bar_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <!-- Background -->\n  <item>\n    <shape>\n      <solid android:color=\"@color/com_braze_content_cards_unread_bar_color\"/>\n      <corners\n        android:bottomLeftRadius=\"@dimen/com_braze_card_background_corner_radius\"\n        android:bottomRightRadius=\"@dimen/com_braze_card_background_corner_radius\"/>\n    </shape>\n  </item>\n</layer-list>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_inappmessage_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/com_braze_inappmessage_button_bg_light\"/>\n    <corners android:radius=\"@dimen/com_braze_inappmessage_button_corner_radius\"/>\n</shape>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_inappmessage_icon_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/com_braze_inappmessage_icon_background\" />\n    <corners android:radius=\"10dp\"/>\n</shape>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_inappmessage_modal_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/com_braze_inappmessage_background_light\" />\n    <corners android:radius=\"10.0dp\"/>\n</shape>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi/com_braze_inappmessage_slideup_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/com_braze_inappmessage_background_slideup\"/>\n    <corners android:radius=\"5dp\"/>\n</shape>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/drawable-nodpi-v21/com_braze_inappmessage_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:color=\"@color/com_braze_inappmessage_button_ripple\" >\n\n  <!-- If no mask layer is set, the ripple effect is masked against the composite of the child layers.-->\n  <item android:id=\"@+id/com_braze_inappmessage_button_background_ripple_internal_gradient\">\n    <shape\n      android:shape=\"rectangle\">\n    <solid android:color=\"@color/com_braze_inappmessage_button_bg_light\"/>\n    <corners android:radius=\"@dimen/com_braze_inappmessage_button_corner_radius\"/>\n    </shape>\n  </item>\n\n</ripple>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_banner_image_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <ViewStub\n        android:id=\"@+id/com_braze_banner_image_card_imageview_stub\"\n        android:layout=\"@layout/com_braze_stubbed_feed_image_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        style=\"@style/Braze.Cards.BannerImage.Image\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_banner_image_content_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                style=\"@style/Braze.ContentCards.BannerImage.Root\">\n    <ImageView\n      android:id=\"@+id/com_braze_content_cards_banner_image_card_image\"\n      style=\"@style/Braze.ContentCards.BannerImage.Image\"/>\n\n    <ImageView\n      android:id=\"@+id/com_braze_content_cards_pinned_icon\"\n      style=\"@style/Braze.ContentCards.PinnedIcon.Banner\"/>\n\n    <View\n      android:id=\"@+id/com_braze_content_cards_unread_bar\"\n      style=\"@style/Braze.ContentCards.UnreadBar.Banner\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_captioned_image_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <ViewStub\n      android:id=\"@+id/com_braze_captioned_image_card_imageview_stub\"\n      android:layout=\"@layout/com_braze_stubbed_feed_image_view\"\n      android:inflatedId=\"@+id/com_braze_captioned_image_card_image\"\n      style=\"@style/Braze.Cards.CaptionedImage.Image\"/>\n\n    <RelativeLayout\n        android:id=\"@+id/com_braze_captioned_image_card_title_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/com_braze_captioned_image_card_image\"\n        style=\"@style/Braze.Cards.CaptionedImage.Title.Container\">\n        <include layout=\"@layout/com_braze_feed_read_indicator_holder\"/>\n\n        <TextView\n            android:id=\"@+id/com_braze_captioned_image_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"10.0dp\"\n            android:layout_marginRight=\"10.0dp\"\n            android:layout_marginTop=\"11.0dp\"\n            android:layout_marginBottom=\"11.0dp\"\n            style=\"@style/Braze.Cards.CaptionedImage.Title\"/>\n    </RelativeLayout>\n\n    <TextView\n        android:id=\"@+id/com_braze_captioned_image_description\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/com_braze_captioned_image_card_title_container\"\n        android:layout_marginLeft=\"10.0dp\"\n        android:layout_marginRight=\"10.0dp\"\n        android:layout_marginTop=\"8.0dp\"\n        style=\"@style/Braze.Cards.CaptionedImage.Description\"/>\n\n    <!-- Optional -->\n    <TextView\n        android:id=\"@+id/com_braze_captioned_image_card_domain\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/com_braze_captioned_image_description\"\n        android:layout_marginLeft=\"10.0dp\"\n        android:layout_marginRight=\"10.0dp\"\n        style=\"@style/Braze.Cards.CaptionedImage.Domain\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_captioned_image_content_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                xmlns:tools=\"http://schemas.android.com/tools\"\n                style=\"@style/Braze.ContentCards.CaptionedImage.Root\">\n    <RelativeLayout\n      android:id=\"@+id/com_braze_content_cards_captioned_image_card_image_container\"\n      style=\"@style/Braze.ContentCards.CaptionedImage.ImageContainer\">\n        <ImageView\n          android:id=\"@+id/com_braze_content_cards_captioned_image_card_image\"\n          style=\"@style/Braze.ContentCards.CaptionedImage.Image\"/>\n    </RelativeLayout>\n\n    <ImageView\n      android:id=\"@+id/com_braze_content_cards_pinned_icon\"\n      style=\"@style/Braze.ContentCards.PinnedIcon.CaptionedImage\"/>\n\n    <RelativeLayout\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      style=\"@style/Braze.ContentCards.CaptionedImage.Container\">\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_captioned_image_title\"\n          tools:text=\"Content Card Title\"\n          style=\"@style/Braze.ContentCards.CaptionedImage.Title\"/>\n\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_captioned_image_description\"\n          tools:text=\"Content Card Description\"\n          style=\"@style/Braze.ContentCards.CaptionedImage.Description\"/>\n\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_action_hint\"\n          style=\"@style/Braze.ContentCards.ActionHint.CaptionedImage\"/>\n    </RelativeLayout>\n\n    <View\n      android:id=\"@+id/com_braze_content_cards_unread_bar\"\n      style=\"@style/Braze.ContentCards.UnreadBar.CaptionedImage\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_content_cards.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/braze_content_cards_swipe_container\"\n  android:layout_width=\"fill_parent\"\n  android:layout_height=\"match_parent\">\n  <androidx.recyclerview.widget.RecyclerView\n    android:id=\"@+id/com_braze_content_cards_recycler\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    style=\"@style/Braze.ContentCardsDisplay.Recycler\"/>\n</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_content_cards_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n  <fragment android:name=\"com.braze.ui.contentcards.ContentCardsFragment\"\n      android:id=\"@+id/com_braze_content_cards\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n</LinearLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_content_cards_empty.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              style=\"@style/Braze.ContentCards.EmptyContainer\">\n  <TextView\n    android:id=\"@+id/com_braze_content_cards_network_unavailable\"\n    style=\"@style/Braze.ContentCardsDisplay.Empty\"/>\n</LinearLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_default_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <include layout=\"@layout/com_braze_feed_read_indicator_holder\"/>\n\n  <TextView\n    android:id=\"@+id/tag\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:visibility=\"gone\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_default_content_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout style=\"@style/Braze.ContentCards\">\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_feed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/braze_feed_swipe_container\"\n  android:layout_width=\"fill_parent\"\n  android:layout_height=\"match_parent\">\n  <RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_feed_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ProgressBar\n      android:id=\"@+id/com_braze_feed_loading_spinner\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:layout_centerInParent=\"true\"\n      android:layout_marginBottom=\"15.0sp\">\n    </ProgressBar>\n\n    <LinearLayout\n      android:id=\"@+id/com_braze_feed_network_error\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:layout_centerInParent=\"true\"\n      android:layout_marginBottom=\"15.0sp\"\n      android:orientation=\"vertical\"\n      android:visibility=\"gone\">\n\n      <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        style=\"@style/Braze.Feed.NetworkErrorTitle\"/>\n\n      <TextView\n        android:id=\"@id/android:empty\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        style=\"@style/Braze.Feed.NetworkErrorBody\"/>\n    </LinearLayout>\n\n    <LinearLayout\n      android:id=\"@+id/com_braze_feed_empty_feed\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:layout_centerInParent=\"true\"\n      android:layout_marginBottom=\"15.0sp\"\n      android:orientation=\"vertical\"\n      android:visibility=\"gone\">\n\n      <TextView\n        android:gravity=\"center_horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        style=\"@style/Braze.Feed.Empty\"/>\n    </LinearLayout>\n\n    <ListView\n      android:id=\"@android:id/list\"\n      android:visibility=\"gone\"\n      android:layout_width=\"@dimen/com_braze_feed_max_width\"\n      android:layout_height=\"match_parent\"\n      android:layout_centerHorizontal=\"true\"\n      style=\"@style/Braze.Feed.List\"\n      />\n\n    <View android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:id=\"@+id/com_braze_feed_transparent_full_bounds_container_view\"/>\n  </RelativeLayout>\n</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_feed_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n  <fragment android:name=\"com.braze.ui.BrazeFeedFragment\"\n      android:id=\"@+id/com_braze_feed\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n</LinearLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_feed_footer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tag\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"0dp\"/>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_feed_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tag\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"0dp\"/>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_feed_read_indicator_holder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"match_parent\">\n  <com.braze.ui.feed.BrazeImageSwitcher\n    android:id=\"@+id/com_braze_newsfeed_item_read_indicator_image_switcher\"\n    style=\"@style/Braze.Cards.ImageSwitcher\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_full.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageFullView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"0.0dp\">\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_full_frame\"\n        style=\"@style/Braze.InAppMessage.Frame.Full\"/>\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_full\"\n        style=\"@style/Braze.InAppMessage.Full\">\n        <LinearLayout\n            android:id=\"@+id/com_braze_inappmessage_full_all_content_parent\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\"\n            android:padding=\"0.0dp\">\n            <com.braze.ui.inappmessage.views.InAppMessageImageView\n                android:id=\"@+id/com_braze_inappmessage_full_imageview\"\n                style=\"@style/Braze.InAppMessage.Image.Full\"/>\n            <LinearLayout\n              android:id=\"@+id/com_braze_inappmessage_full_text_and_button_content_parent\"\n              android:padding=\"0.0dp\"\n              style=\"@style/Braze.InAppMessage.Full.TextAndButtonContent\">\n                <ScrollView\n                    android:id=\"@+id/com_braze_inappmessage_full_scrollview\"\n                    style=\"@style/Braze.InAppMessage.ScrollView.Full\">\n                    <LinearLayout\n                        android:id=\"@+id/com_braze_inappmessage_full_text_layout\"\n                        style=\"@style/Braze.InAppMessage.Full.TextArea\"\n                        android:layout_height=\"wrap_content\">\n                        <TextView\n                            android:id=\"@+id/com_braze_inappmessage_full_header_text\"\n                            style=\"@style/Braze.InAppMessage.Header.Full\"\n                            tools:text=\"In App Message Header Text\"/>\n                        <TextView\n                            android:id=\"@+id/com_braze_inappmessage_full_message\"\n                            style=\"@style/Braze.InAppMessage.Message.Full\"\n                            tools:text=\"In App Message Message Text\"/>\n                    </LinearLayout>\n                </ScrollView>\n                <!-- This layout is solely for a single button being present-->\n                <LinearLayout\n                    android:id=\"@+id/com_braze_inappmessage_full_button_layout_single\"\n                    style=\"@style/Braze.InAppMessage.Layout.Button.Full\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:gravity=\"center\"\n                    android:visibility=\"gone\">\n                    <com.braze.ui.inappmessage.views.InAppMessageButton\n                        android:id=\"@+id/com_braze_inappmessage_full_button_single_one\"\n                        style=\"@style/Braze.InAppMessage.Button.Full.Single\"/>\n                </LinearLayout>\n                <!-- This layout is solely for two buttons being present -->\n                <LinearLayout\n                    android:id=\"@+id/com_braze_inappmessage_full_button_layout_dual\"\n                    style=\"@style/Braze.InAppMessage.Layout.Button.Full\"\n                    android:visibility=\"gone\">\n                    <LinearLayout\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        android:gravity=\"center\">\n                        <com.braze.ui.inappmessage.views.InAppMessageButton\n                            android:id=\"@+id/com_braze_inappmessage_full_button_dual_one\"\n                            style=\"@style/Braze.InAppMessage.Button.Full.Dual.One\"/>\n                    </LinearLayout>\n                    <LinearLayout\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        android:gravity=\"center\">\n                        <com.braze.ui.inappmessage.views.InAppMessageButton\n                            android:id=\"@+id/com_braze_inappmessage_full_button_dual_two\"\n                            style=\"@style/Braze.InAppMessage.Button.Full.Dual.Two\"/>\n                    </LinearLayout>\n                </LinearLayout>\n            </LinearLayout>\n        </LinearLayout>\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n            <ImageButton\n                android:id=\"@+id/com_braze_inappmessage_full_close_button\"\n                style=\"@style/Braze.InAppMessage.CloseButton.Full\"/>\n        </RelativeLayout>\n    </RelativeLayout>\n</com.braze.ui.inappmessage.views.InAppMessageFullView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_full_graphic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageFullView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"0.0dp\">\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_full_frame\"\n        style=\"@style/Braze.InAppMessage.Frame.Full.Graphic\"/>\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_full\"\n        style=\"@style/Braze.InAppMessage.Full.Graphic\">\n        <com.braze.ui.inappmessage.views.InAppMessageImageView\n            android:id=\"@+id/com_braze_inappmessage_full_imageview\"\n            style=\"@style/Braze.InAppMessage.Image.Full.Graphic\"/>\n        <!-- This layout is solely for a single button being present-->\n        <LinearLayout\n            android:id=\"@+id/com_braze_inappmessage_full_button_layout_single\"\n            style=\"@style/Braze.InAppMessage.Layout.Button.Full.Graphic\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:gravity=\"center\"\n            android:layout_centerHorizontal=\"true\"\n            android:visibility=\"gone\">\n            <com.braze.ui.inappmessage.views.InAppMessageButton\n                android:id=\"@+id/com_braze_inappmessage_full_button_single_one\"\n                style=\"@style/Braze.InAppMessage.Button.Full.Single.Graphic\"/>\n        </LinearLayout>\n        <!-- This layout is solely for two buttons being present -->\n        <LinearLayout\n            android:id=\"@+id/com_braze_inappmessage_full_button_layout_dual\"\n            style=\"@style/Braze.InAppMessage.Layout.Button.Full.Graphic\"\n            android:visibility=\"gone\">\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\">\n                <com.braze.ui.inappmessage.views.InAppMessageButton\n                    android:id=\"@+id/com_braze_inappmessage_full_button_dual_one\"\n                    style=\"@style/Braze.InAppMessage.Button.Full.Dual.One.Graphic\"/>\n            </LinearLayout>\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\">\n                <com.braze.ui.inappmessage.views.InAppMessageButton\n                    android:id=\"@+id/com_braze_inappmessage_full_button_dual_two\"\n                    style=\"@style/Braze.InAppMessage.Button.Full.Dual.Two.Graphic\"/>\n            </LinearLayout>\n        </LinearLayout>\n        <ImageButton\n            android:id=\"@+id/com_braze_inappmessage_full_close_button\"\n            style=\"@style/Braze.InAppMessage.CloseButton.Full.Graphic\"/>\n    </RelativeLayout>\n</com.braze.ui.inappmessage.views.InAppMessageFullView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_html.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageHtmlView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_inappmessage_html\"\n    style=\"@style/Braze.InAppMessage.Html\">\n\n    <com.braze.ui.inappmessage.views.InAppMessageWebView\n        android:id=\"@+id/com_braze_inappmessage_html_webview\"\n        style=\"@style/Braze.InAppMessage.Html.Webview\"/>\n</com.braze.ui.inappmessage.views.InAppMessageHtmlView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_html_full.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageHtmlFullView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_inappmessage_html_full\"\n    android:orientation=\"vertical\"\n    style=\"@style/Braze.InAppMessage.Html\">\n\n    <com.braze.ui.inappmessage.views.InAppMessageWebView\n        android:id=\"@+id/com_braze_inappmessage_html_full_webview\"\n        style=\"@style/Braze.InAppMessage.Html.Webview\"/>\n</com.braze.ui.inappmessage.views.InAppMessageHtmlFullView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_modal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageModalView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/com_braze_inappmessage_modal_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"0.0dp\">\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_modal_frame\"\n        style=\"@style/Braze.InAppMessage.Frame.Modal\"/>\n    <com.braze.ui.inappmessage.views.InAppMessageBoundedLayout\n        android:id=\"@+id/com_braze_inappmessage_modal\"\n        style=\"@style/Braze.InAppMessage.Modal\">\n        <RelativeLayout\n            android:id=\"@+id/com_braze_inappmessage_modal_image_layout\"\n            style=\"@style/Braze.InAppMessage.Layout.Image.Modal\">\n            <com.braze.ui.inappmessage.views.InAppMessageImageView\n                android:id=\"@+id/com_braze_inappmessage_modal_imageview\"\n                style=\"@style/Braze.InAppMessage.Image.Modal\"/>\n            <TextView\n                android:id=\"@+id/com_braze_inappmessage_modal_icon\"\n                style=\"@style/Braze.InAppMessage.Icon.Modal\"/>\n        </RelativeLayout>\n        <LinearLayout\n            android:id=\"@+id/com_braze_inappmessage_modal_text_and_button_layout\"\n            style=\"@style/Braze.InAppMessage.Layout.ButtonAndText.Modal\">\n            <ScrollView\n                android:id=\"@+id/com_braze_inappmessage_modal_scrollview\"\n                style=\"@style/Braze.InAppMessage.ScrollView.Modal\">\n                <LinearLayout\n                    android:id=\"@+id/com_braze_inappmessage_modal_text_layout\"\n                    style=\"@style/Braze.InAppMessage.Layout.Text.Modal\">\n                    <TextView\n                        android:id=\"@+id/com_braze_inappmessage_modal_header_text\"\n                        style=\"@style/Braze.InAppMessage.Header.Modal\"\n                        tools:text=\"In App Message Header Text\"/>\n                    <TextView\n                        android:id=\"@+id/com_braze_inappmessage_modal_message\"\n                        style=\"@style/Braze.InAppMessage.Message.Modal\"\n                        tools:text=\"In App Message Message Text\"/>\n                </LinearLayout>\n            </ScrollView>\n            <!-- This layout is solely for a single button being present-->\n            <LinearLayout\n                android:id=\"@+id/com_braze_inappmessage_modal_button_layout_single\"\n                style=\"@style/Braze.InAppMessage.Layout.Button.Modal\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:visibility=\"gone\"\n                android:gravity=\"center\">\n                <com.braze.ui.inappmessage.views.InAppMessageButton\n                    android:id=\"@+id/com_braze_inappmessage_modal_button_single_one\"\n                    style=\"@style/Braze.InAppMessage.Button.Modal.Single\"/>\n            </LinearLayout>\n            <!-- This layout is solely for two buttons being present -->\n            <LinearLayout\n                android:id=\"@+id/com_braze_inappmessage_modal_button_layout_dual\"\n                android:visibility=\"gone\"\n                style=\"@style/Braze.InAppMessage.Layout.Button.Modal\">\n                <LinearLayout\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:gravity=\"center\">\n                    <com.braze.ui.inappmessage.views.InAppMessageButton\n                        android:id=\"@+id/com_braze_inappmessage_modal_button_dual_one\"\n                        style=\"@style/Braze.InAppMessage.Button.Modal.Dual.One\"/>\n                </LinearLayout>\n                <LinearLayout\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:gravity=\"center\">\n                    <com.braze.ui.inappmessage.views.InAppMessageButton\n                        android:id=\"@+id/com_braze_inappmessage_modal_button_dual_two\"\n                        style=\"@style/Braze.InAppMessage.Button.Modal.Dual.Two\"/>\n                </LinearLayout>\n            </LinearLayout>\n        </LinearLayout>\n        <ImageButton\n            android:id=\"@+id/com_braze_inappmessage_modal_close_button\"\n            style=\"@style/Braze.InAppMessage.CloseButton.Modal\"/>\n    </com.braze.ui.inappmessage.views.InAppMessageBoundedLayout>\n</com.braze.ui.inappmessage.views.InAppMessageModalView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_modal_graphic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageModalView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_inappmessage_modal_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"0.0dp\">\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_modal_frame\"\n        style=\"@style/Braze.InAppMessage.Frame.Modal.Graphic\"/>\n    <RelativeLayout\n        android:id=\"@+id/com_braze_inappmessage_modal_graphic_bound\"\n        android:layout_width=\"0.0dp\"\n        android:layout_height=\"0.0dp\"\n        android:layout_centerInParent=\"true\"\n        android:padding=\"0.0dp\">\n        <RelativeLayout\n            android:id=\"@+id/com_braze_inappmessage_modal\"\n            style=\"@style/Braze.InAppMessage.Modal.Graphic\">\n            <com.braze.ui.inappmessage.views.InAppMessageImageView\n                android:id=\"@+id/com_braze_inappmessage_modal_imageview\"\n                style=\"@style/Braze.InAppMessage.Image.Modal.Graphic\"/>\n            <RelativeLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginLeft=\"25dp\"\n                android:layout_marginRight=\"25dp\"\n                android:paddingBottom=\"30dp\">\n                <!-- This layout is solely for a single button being present-->\n                <LinearLayout\n                    android:id=\"@+id/com_braze_inappmessage_modal_button_layout_single\"\n                    style=\"@style/Braze.InAppMessage.Layout.Button.Modal.Graphic\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:gravity=\"center\"\n                    android:layout_centerHorizontal=\"true\"\n                    android:visibility=\"gone\">\n                    <com.braze.ui.inappmessage.views.InAppMessageButton\n                        android:id=\"@+id/com_braze_inappmessage_modal_button_single_one\"\n                        style=\"@style/Braze.InAppMessage.Button.Modal.Single.Graphic\"/>\n                </LinearLayout>\n                <!-- This layout is solely for two buttons being present -->\n                <LinearLayout\n                    android:id=\"@+id/com_braze_inappmessage_modal_button_layout_dual\"\n                    style=\"@style/Braze.InAppMessage.Layout.Button.Modal.Graphic\"\n                    android:visibility=\"gone\">\n                    <LinearLayout\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        android:gravity=\"center\">\n                        <com.braze.ui.inappmessage.views.InAppMessageButton\n                            android:id=\"@+id/com_braze_inappmessage_modal_button_dual_one\"\n                            style=\"@style/Braze.InAppMessage.Button.Modal.Dual.One.Graphic\"/>\n                    </LinearLayout>\n                    <LinearLayout\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_weight=\"1\"\n                        android:gravity=\"center\">\n                        <com.braze.ui.inappmessage.views.InAppMessageButton\n                            android:id=\"@+id/com_braze_inappmessage_modal_button_dual_two\"\n                            style=\"@style/Braze.InAppMessage.Button.Modal.Dual.Two.Graphic\"/>\n                    </LinearLayout>\n                </LinearLayout>\n            </RelativeLayout>\n            <ImageButton\n                android:id=\"@+id/com_braze_inappmessage_modal_close_button\"\n                style=\"@style/Braze.InAppMessage.CloseButton.Modal.Graphic\"/>\n        </RelativeLayout>\n    </RelativeLayout>\n</com.braze.ui.inappmessage.views.InAppMessageModalView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_inappmessage_slideup.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageSlideupView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_inappmessage_slideup\"\n    style=\"@style/Braze.InAppMessage.Slideup\">\n    <RelativeLayout\n      android:id=\"@+id/com_braze_inappmessage_slideup_container\"\n      style=\"@style/Braze.InAppMessage.Slideup.Container\">\n        <RelativeLayout\n            android:id=\"@+id/com_braze_inappmessage_slideup_image_layout\"\n            style=\"@style/Braze.InAppMessage.Slideup.ImageContainer\">\n            <com.braze.ui.inappmessage.views.InAppMessageImageView\n                android:id=\"@+id/com_braze_inappmessage_slideup_imageview\"\n                style=\"@style/Braze.InAppMessage.Image.Slideup\"/>\n            <TextView\n                android:id=\"@+id/com_braze_inappmessage_slideup_icon\"\n                style=\"@style/Braze.InAppMessage.Icon.Slideup\"/>\n        </RelativeLayout>\n        <TextView\n            android:id=\"@+id/com_braze_inappmessage_slideup_message\"\n            style=\"@style/Braze.InAppMessage.Message.Slideup\"/>\n        <ImageView\n            android:id=\"@+id/com_braze_inappmessage_slideup_chevron\"\n            style=\"@style/Braze.InAppMessage.Chevron.Slideup\"/>\n    </RelativeLayout>\n</com.braze.ui.inappmessage.views.InAppMessageSlideupView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_notification_inline_image.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/com_braze_inline_image_push_layout\"\n  tools:ignore=\"RtlHardcoded\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  style=\"@style/Braze.Push.InlineImage.Layout\">\n  <LinearLayout\n    android:id=\"@+id/com_braze_inline_image_push_text_area\"\n    style=\"@style/Braze.Push.InlineImage.TextArea.Layout\">\n    <LinearLayout\n      android:id=\"@+id/com_braze_inline_image_push_text_area_header_layout\"\n      style=\"@style/Braze.Push.InlineImage.TextArea.Header.Layout\">\n      <ImageView\n        android:id=\"@+id/com_braze_inline_image_push_app_icon\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.Header.AppIcon\"\n        tools:src=\"@tools:sample/avatars\"\n        tools:ignore=\"ContentDescription\" />\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_app_name_text\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.Header.AppNameText\"\n        tools:text=\"Droidboy\" />\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_header_text_divider\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.Header.TextDivider\" />\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_time_text\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.Header.TimeText\"\n        tools:text=\"11:51 PM\" />\n    </LinearLayout>\n    <LinearLayout\n      android:id=\"@+id/com_braze_inline_image_title_content_layout\"\n      style=\"@style/Braze.Push.InlineImage.TextArea.TitleContent.Layout\">\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_title_text\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.TitleContent.TitleText\"\n        tools:text=\"Push Notification Title Goes Here\" />\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_content_text\"\n        style=\"@style/Braze.Push.InlineImage.TextArea.TitleContent.ContentText\"\n        tools:text=\"Notification Content Goes Here\" />\n    </LinearLayout>\n  </LinearLayout>\n  <ImageView\n    android:id=\"@+id/com_braze_inline_image_push_side_image\"\n    tools:src=\"@tools:sample/backgrounds/scenic\"\n    tools:layout_width=\"48dp\"\n    tools:layout_height=\"96dp\"\n    style=\"@style/Braze.Push.InlineImage.Image.ImageContent\"\n    tools:ignore=\"ContentDescription\" />\n</LinearLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_push_inline_image_constrained.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/com_braze_inline_image_push_layout\"\n  tools:ignore=\"RtlHardcoded\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  style=\"@style/Braze.Push.InlineImageConstrained.Layout\">\n  <LinearLayout\n    android:id=\"@+id/com_braze_inline_image_push_text_area\"\n    style=\"@style/Braze.Push.InlineImageConstrained.TextArea.Layout\">\n    <LinearLayout\n      android:id=\"@+id/com_braze_inline_image_title_content_layout\"\n      style=\"@style/Braze.Push.InlineImageConstrained.TextArea.TitleContent.Layout\">\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_title_text\"\n        style=\"@style/Braze.Push.InlineImageConstrained.TextArea.TitleContent.TitleText\"\n        tools:text=\"Push Notification Title Goes Here\" />\n      <TextView\n        android:id=\"@+id/com_braze_inline_image_push_content_text\"\n        style=\"@style/Braze.Push.InlineImageConstrained.TextArea.TitleContent.ContentText\"\n        tools:text=\"Notification Content Goes Here\" />\n    </LinearLayout>\n  </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_push_story_one_image.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n             android:layout_width=\"match_parent\"\n             android:layout_height=\"wrap_content\">\n  <LinearLayout\n      android:id=\"@+id/com_braze_story_full_layout\"\n      style=\"@style/Braze.Story.FullLayout\">\n    <RelativeLayout\n        android:id=\"@+id/com_braze_story_relative_layout\"\n        style=\"@style/Braze.Story.OneImageLayout\">\n      <ImageView\n          android:id=\"@+id/com_braze_story_image_view\"\n          style=\"@style/Braze.Story.ImageView.OneImage\"/>\n      <ImageButton\n          android:id=\"@+id/com_braze_story_button_previous\"\n          style=\"@style/Braze.Story.ImageButton.OneImage.Left\"/>\n      <ImageButton\n          android:id=\"@+id/com_braze_story_button_next\"\n          style=\"@style/Braze.Story.ImageButton.OneImage.Right\"/>\n    </RelativeLayout>\n    <LinearLayout\n        android:id=\"@+id/com_braze_story_text_view_container\"\n        style=\"@style/Braze.Story.TextViewContainer.Title.OneImage\">\n      <TextView\n          android:id=\"@+id/com_braze_story_text_view\"\n          style=\"@style/Braze.Story.TextView.Title.OneImage\"/>\n    </LinearLayout>\n    <LinearLayout\n        android:id=\"@+id/com_braze_story_text_view_small_container\"\n        style=\"@style/Braze.Story.TextViewContainer.Subtitle.OneImage\">\n      <TextView\n          android:id=\"@+id/com_braze_story_text_view_small\"\n          style=\"@style/Braze.Story.TextView.Subtitle.OneImage\"/>\n    </LinearLayout>\n  </LinearLayout>\n</FrameLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_short_news_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       xmlns:tools=\"http://schemas.android.com/tools\">\n    <include layout=\"@layout/com_braze_feed_read_indicator_holder\"/>\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:id=\"@+id/com_braze_short_news_card_image\">\n        <ViewStub\n            android:id=\"@+id/com_braze_short_news_card_imageview_stub\"\n            android:layout=\"@layout/com_braze_stubbed_feed_image_view\"\n            style=\"@style/Braze.Cards.ShortNews.Image\"/>\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"0.0dp\"\n        android:layout_height=\"10.0dp\"\n        android:layout_below=\"@id/com_braze_short_news_card_image\"/>\n\n    <TextView\n        android:id=\"@+id/com_braze_short_news_card_title\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toRightOf=\"@id/com_braze_short_news_card_image\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_marginRight=\"10.0dp\"\n        android:layout_marginTop=\"8.0dp\"\n        style=\"@style/Braze.Cards.ShortNews.Title\"\n        tools:ignore=\"RtlHardcoded\"/>\n\n    <TextView\n        android:id=\"@+id/com_braze_short_news_card_description\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/com_braze_short_news_card_title\"\n        android:layout_alignLeft=\"@id/com_braze_short_news_card_title\"\n        android:layout_marginRight=\"10.0dp\"\n        style=\"@style/Braze.Cards.ShortNews.Description\"\n        tools:ignore=\"RtlHardcoded\"/>\n\n    <!-- Optional -->\n    <TextView\n        android:id=\"@+id/com_braze_short_news_card_domain\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/com_braze_short_news_card_description\"\n        android:layout_alignLeft=\"@id/com_braze_short_news_card_description\"\n        android:layout_marginRight=\"10.0dp\"\n        style=\"@style/Braze.Cards.ShortNews.Domain\"\n        tools:ignore=\"RtlHardcoded\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_short_news_content_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                xmlns:tools=\"http://schemas.android.com/tools\"\n                style=\"@style/Braze.ContentCards.ShortNews.Root\">\n    <RelativeLayout\n      android:id=\"@+id/com_braze_content_cards_short_news_card_image_container\"\n      style=\"@style/Braze.ContentCards.ShortNews.ImageContainer\">\n        <ImageView\n          android:id=\"@+id/com_braze_content_cards_short_news_card_image\"\n          style=\"@style/Braze.ContentCards.ShortNews.Image\"/>\n    </RelativeLayout>\n\n    <ImageView\n      android:id=\"@+id/com_braze_content_cards_pinned_icon\"\n      style=\"@style/Braze.ContentCards.PinnedIcon.ShortNews\"/>\n\n    <RelativeLayout\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      style=\"@style/Braze.ContentCards.ShortNews.Container\">\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_short_news_card_title\"\n          tools:text=\"Content Card Title\"\n          style=\"@style/Braze.ContentCards.ShortNews.Title\"/>\n\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_short_news_card_description\"\n          tools:text=\"Content Card Description\"\n          style=\"@style/Braze.ContentCards.ShortNews.Description\"/>\n\n        <TextView\n          android:id=\"@+id/com_braze_content_cards_action_hint\"\n          style=\"@style/Braze.ContentCards.ActionHint.ShortNews\"/>\n    </RelativeLayout>\n\n    <View\n      android:id=\"@+id/com_braze_content_cards_unread_bar\"\n      style=\"@style/Braze.ContentCards.UnreadBar.ShortNews\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_stubbed_feed_image_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_stubbed_feed_image_view_parent\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <!-- Due to this ImageView being nested within a layout and style properties not capable of being\n    inherited from the layout parent, any ImageView specific properties, like scaleType, cannot be applied to\n    this ImageView directly from the ViewStub origin xml layout. Thus, any ImageView properties for this\n    view must be set programmatically post-inflation. This ImageView must be wrapped in a layout tag\n    and not a merge tag due to this issue: http://stackoverflow.com/questions/29344787/including-views-using-merge-tag-and-viewstub.\n    -->\n    <ImageView\n        android:id=\"@+id/com_braze_stubbed_feed_image_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_text_announcement_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       xmlns:tools=\"http://schemas.android.com/tools\">\n  <include layout=\"@layout/com_braze_feed_read_indicator_holder\"/>\n  <TextView\n      android:id=\"@+id/com_braze_text_announcement_card_title\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_alignParentTop=\"true\"\n      android:layout_alignParentLeft=\"true\"\n      android:layout_marginLeft=\"10.0dp\"\n      android:layout_marginRight=\"10.0dp\"\n      android:layout_marginTop=\"10.0dp\"\n      style=\"@style/Braze.Cards.TextAnnouncement.Title\"\n      tools:ignore=\"RtlHardcoded\"/>\n\n  <TextView\n      android:id=\"@+id/com_braze_text_announcement_card_description\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_alignParentLeft=\"true\"\n      android:layout_below=\"@id/com_braze_text_announcement_card_title\"\n      android:layout_marginLeft=\"10.0dp\"\n      android:layout_marginRight=\"10.0dp\"\n      style=\"@style/Braze.Cards.TextAnnouncement.Description\"\n      tools:ignore=\"RtlHardcoded\"/>\n\n  <!-- Optional -->\n  <TextView\n      android:id=\"@+id/com_braze_text_announcement_card_domain\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_alignParentLeft=\"true\"\n      android:layout_below=\"@id/com_braze_text_announcement_card_description\"\n      android:layout_marginLeft=\"10.0dp\"\n      android:layout_marginRight=\"10.0dp\"\n      style=\"@style/Braze.Cards.TextAnnouncement.Domain\"\n      tools:ignore=\"RtlHardcoded\"/>\n</merge>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_text_announcement_content_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                xmlns:tools=\"http://schemas.android.com/tools\"\n                style=\"@style/Braze.ContentCards.TextAnnouncement.Root\">\n\n  <RelativeLayout\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    style=\"@style/Braze.ContentCards.TextAnnouncement.Container\">\n    <TextView\n      android:id=\"@+id/com_braze_content_cards_text_announcement_card_title\"\n      tools:text=\"Content Card Title\"\n      style=\"@style/Braze.ContentCards.TextAnnouncement.Title\"/>\n\n    <TextView\n      android:id=\"@+id/com_braze_content_cards_text_announcement_card_description\"\n      tools:text=\"Content Card Description\"\n      style=\"@style/Braze.ContentCards.TextAnnouncement.Description\"/>\n\n    <TextView\n      android:id=\"@+id/com_braze_content_cards_action_hint\"\n      style=\"@style/Braze.ContentCards.ActionHint.TextAnnouncement\"/>\n  </RelativeLayout>\n\n  <ImageView\n    android:id=\"@+id/com_braze_content_cards_pinned_icon\"\n    style=\"@style/Braze.ContentCards.PinnedIcon.TextAnnouncement\"/>\n\n  <View\n    android:id=\"@+id/com_braze_content_cards_unread_bar\"\n    style=\"@style/Braze.ContentCards.UnreadBar.TextAnnouncement\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout/com_braze_webview_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n  <WebView\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:id=\"@+id/com_braze_webview_activity_webview\"\n    android:layout_centerVertical=\"true\"\n    android:layout_centerHorizontal=\"true\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/layout-land/com_braze_inappmessage_html_full.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.braze.ui.inappmessage.views.InAppMessageHtmlFullView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/com_braze_inappmessage_html_full\"\n    android:orientation=\"horizontal\"\n    style=\"@style/Braze.InAppMessage.Html\">\n\n    <com.braze.ui.inappmessage.views.InAppMessageWebView\n        android:id=\"@+id/com_braze_inappmessage_html_full_webview\"\n        style=\"@style/Braze.InAppMessage.Html.Webview\"/>\n</com.braze.ui.inappmessage.views.InAppMessageHtmlFullView>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <declare-styleable name=\"com.braze.ui.feed.BrazeImageSwitcher\">\n    <attr name=\"brazeFeedCustomReadIcon\" format=\"integer\"/>\n    <attr name=\"brazeFeedCustomUnReadIcon\" format=\"integer\"/>\n  </declare-styleable>\n  <declare-styleable name=\"InAppMessageBoundedLayout\">\n    <attr name=\"inAppMessageBoundedLayoutMaxWidth\" format=\"dimension\"/>\n    <attr name=\"inAppMessageBoundedLayoutMinWidth\" format=\"dimension\"/>\n    <attr name=\"inAppMessageBoundedLayoutMaxHeight\" format=\"dimension\"/>\n    <attr name=\"inAppMessageBoundedLayoutMinHeight\" format=\"dimension\"/>\n  </declare-styleable>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"com_braze_inappmessage_icon\">@android:color/white</color>\n  <color name=\"com_braze_inappmessage_icon_background\">#ff0073d5</color>\n  <color name=\"com_braze_inappmessage_chevron\">#9B9B9B</color>\n  <color name=\"com_braze_inappmessage_frame_light\">#C0333333</color>\n\n  <color name=\"com_braze_inappmessage_background_slideup\">#efefef</color>\n  <color name=\"com_braze_inappmessage_text_slideup\">#555555</color>\n\n  <color name=\"com_braze_inappmessage_background_light\">@android:color/white</color>\n  <color name=\"com_braze_inappmessage_header_text\">#333333</color>\n  <color name=\"com_braze_inappmessage_text\">#555555</color>\n\n  <color name=\"com_braze_inappmessage_button_text_light\">@android:color/white</color>\n  <color name=\"com_braze_inappmessage_button_bg_light\">#1B78CF</color>\n  <color name=\"com_braze_inappmessage_button_ripple\">#5f000000</color>\n\n  <color name=\"com_braze_title\">#3F3F3F</color>\n  <color name=\"com_braze_description\">#666565</color>\n  <color name=\"com_braze_domain\">#3F3F3F</color>\n\n  <color name=\"com_braze_card_title_container\">#F5F5F5</color>\n\n  <color name=\"com_braze_card_background_border\">#50CCCCCC</color>\n  <color name=\"com_braze_card_background_shadow\">#30CCCCCC</color>\n  <color name=\"com_braze_card_background\">@android:color/white</color>\n\n  <color name=\"com_braze_newsfeed_swipe_refresh_color_1\">#f33e3e</color>\n  <color name=\"com_braze_newsfeed_swipe_refresh_color_2\">#1bbdf2</color>\n  <color name=\"com_braze_newsfeed_swipe_refresh_color_3\">#2f3436</color>\n  <color name=\"com_braze_newsfeed_swipe_refresh_color_4\">#ffffff</color>\n\n  <color name=\"com_braze_content_card_empty_text_color\">#3F3F3F</color>\n  <color name=\"com_braze_content_card_background_border\">#50CCCCCC</color>\n  <color name=\"com_braze_content_card_background_shadow\">#30CCCCCC</color>\n  <color name=\"com_braze_content_card_background\">@android:color/white</color>\n  <color name=\"com_braze_content_card_focus_scrim\">#30333333</color>\n\n  <color name=\"com_braze_content_cards_swipe_refresh_color_1\">#f33e3e</color>\n  <color name=\"com_braze_content_cards_swipe_refresh_color_2\">#1bbdf2</color>\n  <color name=\"com_braze_content_cards_swipe_refresh_color_3\">#2f3436</color>\n  <color name=\"com_braze_content_cards_swipe_refresh_color_4\">#ffffff</color>\n\n  <!-- The color used to highlight unread Content Cards at their bottom edge -->\n  <color name=\"com_braze_content_cards_unread_bar_color\">#1676d0</color>\n\n  <!-- The color of the \"Read More >\" text when card actions are present -->\n  <color name=\"com_braze_content_cards_action_hint_text_color\">#1676D0</color>\n\n  <color name=\"com_braze_content_cards_title\">#3F3F3F</color>\n  <color name=\"com_braze_content_cards_description\">#666565</color>\n\n  <color name=\"com_braze_content_cards_display_background_color\">#EEE</color>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_feed_max_width\">380.0dp</dimen>\n\n  <dimen name=\"com_braze_card_background_border_left\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_right\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_top\">0.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_shadow_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_corner_radius\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_shadow_radius\">2.0dp</dimen>\n\n  <dimen name=\"com_braze_content_card_background_border_left\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_right\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_top\">0.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_corner_radius\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_radius\">2.0dp</dimen>\n\n  <dimen name=\"com_braze_content_cards_max_width\">380.0dp</dimen>\n  <dimen name=\"com_braze_content_cards_divider_height\">32dp</dimen>\n  <dimen name=\"com_braze_content_cards_divider_left_margin\">10dp</dimen>\n  <dimen name=\"com_braze_content_cards_divider_right_margin\">10dp</dimen>\n\n  <dimen name=\"com_braze_content_cards_unread_bar_height\">4dp</dimen>\n  <dimen name=\"com_braze_content_cards_image_border_radius\">3dp</dimen>\n\n  <dimen name=\"com_braze_inappmessage_modal_min_width\">320dp</dimen>\n  <dimen name=\"com_braze_inappmessage_modal_max_width\">450dp</dimen>\n  <dimen name=\"com_braze_inappmessage_modal_max_height\">750dp</dimen>\n  <dimen name=\"com_braze_inappmessage_modal_margin\">15dp</dimen>\n  <dimen name=\"com_braze_inappmessage_slideup_left_message_margin_no_image\">20dp</dimen>\n  <dimen name=\"com_braze_inappmessage_slideup_max_width\">450dp</dimen>\n  <!-- The click area target size in dp for modals and fullscreen in-app messages to ensure the click area extends beyond the drawable. -->\n  <dimen name=\"com_braze_inappmessage_close_button_click_area_width\">48dp</dimen>\n  <dimen name=\"com_braze_inappmessage_close_button_click_area_height\">48dp</dimen>\n\n  <dimen name=\"com_braze_inappmessage_button_corner_radius\">5dp</dimen>\n  <!-- The width of the stroke used in drawing the button color -->\n  <dimen name=\"com_braze_inappmessage_button_border_stroke\">1dp</dimen>\n  <dimen name=\"com_braze_inappmessage_button_border_stroke_focused\">4dp</dimen>\n\n  <!-- Inline Image Push -->\n  <!-- The margin before and after each of the items in the notification header. -->\n  <dimen name=\"com_braze_push_inline_image_header_separating_margin\">2dp</dimen>\n  <!-- Controls the ratio of image to the rest of the notification. The weights should add up to the sum. -->\n  <dimen name=\"com_braze_push_inline_image_total_weight_sum\">10</dimen>\n  <dimen name=\"com_braze_push_inline_image_text_area_layout_weight\">6</dimen>\n  <dimen name=\"com_braze_push_inline_image_image_layout_weight\">4</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- Application specific identifiers for View tagging, required for Glide image library compatibility. -->\n  <string translatable=\"false\" name=\"com_braze_image_resize_tag_key\">com_appboy_image_resize_tag_key</string>\n  <string translatable=\"false\" name=\"com_braze_image_is_read_tag_key\">com_appboy_image_is_read_tag_key</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">We have no updates.\\nPlease check again later.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Connection Error</string>\n  <string name=\"com_braze_feed_connection_error_body\">Cannot establish network connection.\\nPlease try again later.</string>\n\n  <!-- Content descriptions -->\n  <string name=\"com_braze_inappmessage_close_content_description\">Close</string>\n  <string name=\"com_braze_inappmessage_image_content_description\" translatable=\"false\">@null</string>\n  <string name=\"com_braze_inappmessage_icon_content_description\" translatable=\"false\">@null</string>\n\n  <!--  Inline Image Push -->\n  <string name=\"com_braze_inline_image_push_notification_header_divider_symbol\" translatable=\"false\">•</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"Braze\"/>\n\n  <!-- In-app Message -->\n  <style name=\"Braze.InAppMessage\">\n  </style>\n  <style name=\"Braze.InAppMessage.Header\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:background\">@android:color/transparent</item>\n    <item name=\"android:textColor\">@color/com_braze_inappmessage_header_text</item>\n    <item name=\"android:textSize\">20.0sp</item>\n    <item name=\"android:lineSpacingMultiplier\">1.3</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:layout_centerHorizontal\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Message\">\n    <item name=\"android:background\">@android:color/transparent</item>\n    <item name=\"android:textSize\">14.0sp</item>\n    <item name=\"android:lineSpacingMultiplier\">1.1</item>\n    <item name=\"android:lineSpacingExtra\">0dp</item>\n    <item name=\"android:textColor\">@color/com_braze_inappmessage_text</item>\n  </style>\n  <style name=\"Braze.InAppMessage.ScrollView\">\n    <item name=\"android:fillViewport\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Image\">\n  </style>\n  <style name=\"Braze.InAppMessage.Icon\">\n    <item name=\"android:layout_height\">50.0dp</item>\n    <item name=\"android:layout_width\">50.0dp</item>\n    <item name=\"android:textSize\">30.0sp</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:layout_gravity\">center</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_icon_background</item>\n    <item name=\"android:textColor\">@color/com_braze_inappmessage_icon</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:padding\">0.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Image\">\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.ButtonAndText\">\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Button\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:orientation\">horizontal</item>\n    <item name=\"android:weightSum\">2.0</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Text\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_centerHorizontal\">true</item>\n    <item name=\"android:orientation\">vertical</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button\">\n    <item name=\"android:layout_height\">44dp</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:textColor\">@color/com_braze_inappmessage_button_text_light</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_button_background</item>\n    <item name=\"android:textSize\">14.0sp</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:layout_gravity\">center</item>\n    <item name=\"android:singleLine\">true</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:paddingLeft\">12dp</item>\n    <item name=\"android:paddingRight\">12dp</item>\n    <item name=\"android:textAllCaps\">false</item>\n    <item name=\"android:minWidth\">80dp</item>\n    <item name=\"android:layout_weight\">1</item>\n    <item name=\"android:focusable\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.CloseButton\">\n    <item name=\"android:contentDescription\">@string/com_braze_inappmessage_close_content_description</item>\n    <item name=\"android:layout_height\">20dp</item>\n    <item name=\"android:layout_width\">20dp</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_close_button_selector</item>\n    <item name=\"android:layout_marginRight\">15dp</item>\n    <item name=\"android:layout_marginTop\">15dp</item>\n    <item name=\"android:foreground\">?android:attr/selectableItemBackgroundBorderless</item>\n    <item name=\"android:focusable\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Chevron\">\n    <item name=\"android:layout_height\">20dp</item>\n    <item name=\"android:layout_width\">12dp</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_chevron</item>\n    <item name=\"android:layout_marginRight\">20dp</item>\n    <item name=\"android:layout_marginLeft\">18dp</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:layout_centerVertical\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Frame\">\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:background\">@color/com_braze_inappmessage_frame_light</item>\n  </style>\n\n  <!-- In-App Message Slideup -->\n  <style name=\"Braze.InAppMessage.Slideup\">\n    <item name=\"android:layout_height\">60.0dp</item>\n    <item name=\"android:layout_width\">match_parent</item>\n      <item name=\"android:gravity\">center</item>\n  </style>\n  <!--\n  On pre API 19 devices, the slideup won't have the proper bottom\n  margin that makes it \"float\". Adding padding here or changing the height flag\n  doesn't work either. This is a confirmed bug in pre API 19\n  (https://android.googlesource.com/platform/frameworks/base/+/android-4.2.2_r1.1/core/java/android/widget/RelativeLayout.java#487)\n  that is confirmed fixed in API 19\n  (https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1/core/java/android/widget/RelativeLayout.java#498)\n  -->\n  <style name=\"Braze.InAppMessage.Slideup.Container\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">@dimen/com_braze_inappmessage_slideup_max_width</item>\n    <item name=\"android:layout_marginLeft\">10dp</item>\n    <item name=\"android:layout_marginRight\">10dp</item>\n    <item name=\"android:layout_marginBottom\">10dp</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_slideup_background</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Message.Slideup\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:textColor\">@color/com_braze_inappmessage_text_slideup</item>\n    <item name=\"android:textSize\">14.0sp</item>\n    <item name=\"android:maxLines\">3</item>\n    <item name=\"android:gravity\">start</item>\n    <item name=\"android:layout_gravity\">center_vertical</item>\n    <item name=\"android:layout_centerVertical\">true</item>\n    <item name=\"android:layout_toRightOf\">@id/com_braze_inappmessage_slideup_image_layout</item>\n    <item name=\"android:layout_toLeftOf\">@id/com_braze_inappmessage_slideup_chevron</item>\n    <item name=\"android:layout_marginTop\">15dp</item>\n    <item name=\"android:layout_marginBottom\">15dp</item>\n    <item name=\"android:layout_marginRight\">20dp</item>\n    <item name=\"android:paddingBottom\">15dp</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:ellipsize\">end</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Slideup.ImageContainer\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_centerVertical\">true</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginRight\">10dp</item>\n    <item name=\"android:layout_marginLeft\">15dp</item>\n    <item name=\"android:layout_marginTop\">15dp</item>\n    <item name=\"android:layout_marginBottom\">15dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Image.Slideup\">\n    <item name=\"android:contentDescription\">@string/com_braze_inappmessage_image_content_description</item>\n    <item name=\"android:layout_height\">50.0dp</item>\n    <item name=\"android:layout_width\">50.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Icon.Slideup\">\n    <item name=\"android:contentDescription\">@string/com_braze_inappmessage_icon_content_description</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Chevron.Slideup\">\n    <item name=\"android:layout_marginLeft\">0dp</item>\n  </style>\n\n  <!-- In-App Message Modal -->\n  <style name=\"Braze.InAppMessage.Modal\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:paddingTop\">0.0dp</item>\n    <item name=\"android:paddingRight\">0.0dp</item>\n    <item name=\"android:paddingLeft\">0.0dp</item>\n    <item name=\"android:paddingBottom\">20.0dp</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_modal_background</item>\n    <item name=\"android:layout_margin\">15dp</item>\n    <item name=\"inAppMessageBoundedLayoutMinWidth\">@dimen/com_braze_inappmessage_modal_min_width</item>\n    <item name=\"inAppMessageBoundedLayoutMaxWidth\">@dimen/com_braze_inappmessage_modal_max_width</item>\n    <item name=\"inAppMessageBoundedLayoutMaxHeight\">@dimen/com_braze_inappmessage_modal_max_height</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Header.Modal\">\n    <item name=\"android:layout_width\">match_parent</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Message.Modal\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:layout_gravity\">center</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:layout_marginTop\">10.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Image.Modal\">\n    <item name=\"android:contentDescription\">@string/com_braze_inappmessage_image_content_description</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:layout_marginTop\">20.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Image.Modal\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Icon.Modal\">\n    <item name=\"android:layout_marginTop\">20.0dp</item>\n    <item name=\"android:layout_centerHorizontal\">true</item>\n    <item name=\"android:layout_gravity\">center</item>\n  </style>\n  <style name=\"Braze.InAppMessage.ScrollView.Modal\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_weight\">1.0</item>\n    <item name=\"android:layout_marginTop\">20.0dp</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:layout_gravity\">center</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.ButtonAndText.Modal\">\n    <item name=\"android:layout_below\">@id/com_braze_inappmessage_modal_image_layout</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:layout_gravity\">center</item>\n    <item name=\"android:layout_marginLeft\">20.0dp</item>\n    <item name=\"android:layout_marginRight\">20.0dp</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:weightSum\">1.0</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Button.Modal\">\n    <item name=\"android:layout_marginTop\">20.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Text.Modal\">\n    <item name=\"android:orientation\">vertical</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Single\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Dual\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Dual.One\">\n    <item name=\"android:layout_marginRight\">5dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Dual.Two\">\n    <item name=\"android:layout_marginLeft\">5dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.CloseButton.Modal\">\n  </style>\n  <style name=\"Braze.InAppMessage.Frame.Modal\">\n  </style>\n\n  <!-- In-App Message Graphic Modal -->\n  <style name=\"Braze.InAppMessage.Modal.Graphic\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:layout_margin\">0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Image.Modal.Graphic\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Button.Modal.Graphic\">\n    <item name=\"android:layout_below\">@id/com_braze_inappmessage_modal_text_layout</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_alignParentBottom\">true</item>\n  </style>\n\n  <style name=\"Braze.InAppMessage.Button.Modal.Single.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Dual.One.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Modal.Dual.Two.Graphic\">\n  </style>\n\n  <style name=\"Braze.InAppMessage.CloseButton.Modal.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Frame.Modal.Graphic\">\n  </style>\n\n  <!-- In-App Message Full -->\n  <style name=\"Braze.InAppMessage.Full\">\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:background\">@color/com_braze_inappmessage_background_light</item>\n    <item name=\"android:padding\">0.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Full.TextArea\">\n    <item name=\"android:background\">@android:color/transparent</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Full.TextAndButtonContent\">\n    <item name=\"android:layout_marginBottom\">30dp</item>\n    <item name=\"android:layout_marginTop\">30dp</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Header.Full\">\n  </style>\n  <style name=\"Braze.InAppMessage.Message.Full\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:padding\">0.0dp</item>\n    <item name=\"android:layout_marginTop\">10dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.ScrollView.Full\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginLeft\">25dp</item>\n    <item name=\"android:layout_marginRight\">25dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Image.Full\">\n    <item name=\"android:contentDescription\">@string/com_braze_inappmessage_image_content_description</item>\n    <item name=\"android:layout_centerHorizontal\">true</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">0.0dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Button.Full\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_marginTop\">20dp</item>\n    <item name=\"android:layout_marginRight\">25dp</item>\n    <item name=\"android:layout_marginLeft\">25dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Single\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Dual\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Dual.One\">\n    <item name=\"android:layout_marginRight\">5dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Dual.Two\">\n    <item name=\"android:layout_marginLeft\">5dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.CloseButton.Full\">\n    <item name=\"android:layout_marginRight\">25dp</item>\n    <item name=\"android:layout_marginTop\">25dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Frame.Full\">\n  </style>\n\n  <!-- In-App Message Graphic Full -->\n  <style name=\"Braze.InAppMessage.Full.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Image.Full.Graphic\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Layout.Button.Full.Graphic\">\n    <item name=\"android:layout_alignParentBottom\">true</item>\n    <item name=\"android:layout_marginBottom\">30dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Single.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Dual.One.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Button.Full.Dual.Two.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.Frame.Full.Graphic\">\n  </style>\n  <style name=\"Braze.InAppMessage.CloseButton.Full.Graphic\">\n  </style>\n\n  <!-- In-App Message Html Full -->\n  <style name=\"Braze.InAppMessage.Html\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Html.Webview\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n  </style>\n\n  <!-- Feed -->\n  <style name=\"Braze.Feed\"/>\n  <!-- Styling the ListView background will require you to set the cacheColorHint, otherwise the\n   background may appear black on older versions of Android. For more information about the issue,\n   see http://android-developers.blogspot.com/2009/01/why-is-my-list-black-android.html -->\n  <style name=\"Braze.Feed.List\">\n    <item name=\"android:background\">@android:color/transparent</item>\n    <item name=\"android:divider\">@android:color/transparent</item>\n    <item name=\"android:dividerHeight\">16.0dp</item>\n    <item name=\"android:paddingLeft\">12.5dp</item>\n    <item name=\"android:paddingRight\">5.0dp</item>\n    <item name=\"android:scrollbarStyle\">outsideInset</item>\n  </style>\n  <style name=\"Braze.Feed.Empty\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:text\">@string/com_braze_feed_empty</item>\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">18.0sp</item>\n  </style>\n  <style name=\"Braze.Feed.NetworkErrorTitle\">\n    <item name=\"android:gravity\">center_horizontal</item>\n    <item name=\"android:text\">@string/com_braze_feed_connection_error_title</item>\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">19.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n  </style>\n  <style name=\"Braze.Feed.NetworkErrorBody\">\n    <item name=\"android:gravity\">center_horizontal</item>\n    <item name=\"android:text\">@string/com_braze_feed_connection_error_body</item>\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">15.0sp</item>\n  </style>\n\n  <!-- Legacy News Feed Cards -->\n  <style name=\"Braze.Cards\"/>\n  <style name=\"Braze.Cards.ImageSwitcher\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:gravity\">right|top</item>\n  </style>\n\n  <!-- Banner Card -->\n  <style name=\"Braze.Cards.BannerImage\"/>\n  <style name=\"Braze.Cards.BannerImage.Image\">\n  </style>\n\n  <!-- Captioned Image Card -->\n  <style name=\"Braze.Cards.CaptionedImage\"/>\n  <style name=\"Braze.Cards.CaptionedImage.Image\">\n    <item name=\"android:layout_alignParentTop\">true</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.Cards.CaptionedImage.Title.Container\">\n    <item name=\"android:background\">@color/com_braze_card_title_container</item>\n  </style>\n  <style name=\"Braze.Cards.CaptionedImage.Title\">\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">17.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n  </style>\n  <style name=\"Braze.Cards.CaptionedImage.Description\">\n    <item name=\"android:textColor\">@color/com_braze_description</item>\n    <item name=\"android:textSize\">15.0sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">8.0dp</item>\n  </style>\n  <style name=\"Braze.Cards.CaptionedImage.Domain\">\n    <item name=\"android:textColor\">@color/com_braze_domain</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">10.0dp</item>\n  </style>\n\n  <!-- Text Announcement Card -->\n  <style name=\"Braze.Cards.TextAnnouncement\"/>\n  <style name=\"Braze.Cards.TextAnnouncement.Title\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">16.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">8.0dp</item>\n  </style>\n  <style name=\"Braze.Cards.TextAnnouncement.Description\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_description</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">8.0dp</item>\n  </style>\n  <style name=\"Braze.Cards.TextAnnouncement.Domain\">\n    <item name=\"android:textColor\">@color/com_braze_domain</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">10.0dp</item>\n  </style>\n\n  <!-- Short News Card -->\n  <style name=\"Braze.Cards.ShortNews\"/>\n  <style name=\"Braze.Cards.ShortNews.Image\">\n    <item name=\"android:layout_width\">57.5dp</item>\n    <item name=\"android:layout_height\">57.5dp</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginLeft\">10.0dp</item>\n    <item name=\"android:layout_marginRight\">10.0dp</item>\n    <item name=\"android:layout_marginTop\">10.0dp</item>\n  </style>\n  <style name=\"Braze.Cards.ShortNews.Title\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_title</item>\n    <item name=\"android:textSize\">16.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:singleLine\">true</item>\n    <item name=\"android:includeFontPadding\">false</item>\n  </style>\n  <style name=\"Braze.Cards.ShortNews.Description\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_description</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingTop\">8.0dp</item>\n    <item name=\"android:paddingBottom\">8.0dp</item>\n  </style>\n  <style name=\"Braze.Cards.ShortNews.Domain\">\n    <item name=\"android:textColor\">@color/com_braze_domain</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:paddingBottom\">10.0dp</item>\n  </style>\n\n  <!-- Content -->\n  <style name=\"Braze.ContentCardsDisplay\"/>\n  <style name=\"Braze.ContentCardsDisplay.Recycler\">\n    <item name=\"android:background\">@color/com_braze_content_cards_display_background_color</item>\n    <item name=\"android:divider\">@android:color/transparent</item>\n    <item name=\"android:scrollbarStyle\">outsideInset</item>\n    <item name=\"android:scrollbars\">vertical</item>\n  </style>\n  <style name=\"Braze.ContentCardsDisplay.Empty\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:text\">@string/com_braze_feed_empty</item>\n    <item name=\"android:textColor\">@color/com_braze_content_card_empty_text_color</item>\n    <item name=\"android:textSize\">18.0sp</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:layout_width\">match_parent</item>\n  </style>\n\n  <!-- Content Cards -->\n  <style name=\"Braze.ContentCards\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:orientation\">horizontal</item>\n    <item name=\"android:focusable\">true</item>\n  </style>\n  <style name=\"Braze.ContentCards.EmptyContainer\">\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:layout_centerVertical\">true</item>\n    <item name=\"android:orientation\">vertical</item>\n  </style>\n  <style name=\"Braze.ContentCards.UnreadBar\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">@dimen/com_braze_content_cards_unread_bar_height</item>\n    <item name=\"android:background\">@color/com_braze_content_cards_unread_bar_color</item>\n    <item name=\"android:layout_weight\">1</item>\n    <item name=\"android:layout_alignParentBottom\">true</item>\n  </style>\n  <style name=\"Braze.ContentCards.PinnedIcon\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n    <item name=\"android:src\">@drawable/com_braze_content_card_icon_pinned</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:importantForAccessibility\">no</item>\n  </style>\n  <style name=\"Braze.ContentCards.ActionHint\">\n    <item name=\"android:textSize\">13sp</item>\n    <item name=\"android:lineSpacingMultiplier\">1.2</item>\n    <item name=\"android:textColor\">@color/com_braze_content_cards_action_hint_text_color</item>\n  </style>\n\n  <!-- Captioned Image Content Card -->\n  <style name=\"Braze.ContentCards.CaptionedImage\">\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.Root\">\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_content_cards_divider_left_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_content_cards_divider_right_margin</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.ImageContainer\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.Image\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:scaleType\">centerCrop</item>\n    <item name=\"android:adjustViewBounds\">true</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:importantForAccessibility\">no</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.Container\">\n    <item name=\"android:layout_marginLeft\">25dp</item>\n    <item name=\"android:layout_marginRight\">25dp</item>\n    <item name=\"android:layout_marginTop\">20dp</item>\n    <item name=\"android:paddingBottom\">25dp</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_captioned_image_card_image_container</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.Title\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:textColor\">@color/com_braze_content_cards_title</item>\n    <item name=\"android:textSize\">16sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n  </style>\n  <style name=\"Braze.ContentCards.CaptionedImage.Description\">\n    <item name=\"android:textColor\">@color/com_braze_content_cards_description</item>\n    <item name=\"android:textSize\">13sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_marginTop\">10dp</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_captioned_image_title</item>\n  </style>\n  <style name=\"Braze.ContentCards.ActionHint.CaptionedImage\">\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginTop\">12dp</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_captioned_image_description</item>\n  </style>\n  <style name=\"Braze.ContentCards.PinnedIcon.CaptionedImage\"/>\n  <style name=\"Braze.ContentCards.UnreadBar.CaptionedImage\"/>\n\n  <!-- Banner Content Card -->\n  <style name=\"Braze.ContentCards.BannerImage\"/>\n  <style name=\"Braze.ContentCards.BannerImage.Root\">\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_content_cards_divider_left_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_content_cards_divider_right_margin</item>\n  </style>\n  <style name=\"Braze.ContentCards.BannerImage.Image\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">250dp</item>\n    <item name=\"android:scaleType\">centerCrop</item>\n    <item name=\"android:adjustViewBounds\">true</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:importantForAccessibility\">no</item>\n  </style>\n  <style name=\"Braze.ContentCards.PinnedIcon.Banner\"/>\n  <style name=\"Braze.ContentCards.UnreadBar.Banner\"/>\n\n  <!-- Short News Content Card -->\n  <style name=\"Braze.ContentCards.ShortNews\"/>\n  <style name=\"Braze.ContentCards.ShortNews.Root\">\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_content_cards_divider_left_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_content_cards_divider_right_margin</item>\n  </style>\n  <style name=\"Braze.ContentCards.ShortNews.ImageContainer\">\n    <item name=\"android:layout_width\">57.5dp</item>\n    <item name=\"android:layout_height\">57.5dp</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginLeft\">25dp</item>\n    <item name=\"android:layout_marginTop\">20dp</item>\n    <item name=\"android:layout_marginBottom\">25.0dp</item>\n  </style>\n  <style name=\"Braze.ContentCards.ShortNews.Image\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:scaleType\">centerCrop</item>\n    <item name=\"android:adjustViewBounds\">true</item>\n    <item name=\"android:background\">@drawable/com_braze_content_cards_rounded_corner_background</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:importantForAccessibility\">no</item>\n  </style>\n  <style name=\"Braze.ContentCards.ShortNews.Container\">\n    <item name=\"android:layout_marginLeft\">12dp</item>\n    <item name=\"android:layout_marginTop\">20dp</item>\n    <item name=\"android:paddingBottom\">25dp</item>\n    <item name=\"android:layout_marginRight\">25dp</item>\n    <item name=\"android:layout_toRightOf\">@id/com_braze_content_cards_short_news_card_image_container</item>\n  </style>\n  <style name=\"Braze.ContentCards.ShortNews.Title\">\n    <item name=\"android:textColor\">@color/com_braze_content_cards_title</item>\n    <item name=\"android:textSize\">16.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n  </style>\n  <style name=\"Braze.ContentCards.ShortNews.Description\">\n    <item name=\"android:textColor\">@color/com_braze_content_cards_description</item>\n    <item name=\"android:textSize\">13sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_short_news_card_title</item>\n    <item name=\"android:layout_marginTop\">10.0dp</item>\n  </style>\n  <style name=\"Braze.ContentCards.ActionHint.ShortNews\">\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginTop\">12dp</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_short_news_card_description</item>\n  </style>\n  <style name=\"Braze.ContentCards.PinnedIcon.ShortNews\"/>\n  <style name=\"Braze.ContentCards.UnreadBar.ShortNews\"/>\n\n  <!-- Text Announcement Content Card -->\n  <style name=\"Braze.ContentCards.TextAnnouncement\">\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.ContentCards.TextAnnouncement.Root\">\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_content_cards_divider_left_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_content_cards_divider_right_margin</item>\n  </style>\n  <style name=\"Braze.ContentCards.TextAnnouncement.Container\">\n    <item name=\"android:layout_marginLeft\">25dp</item>\n    <item name=\"android:layout_marginRight\">25dp</item>\n    <item name=\"android:layout_marginTop\">20dp</item>\n    <item name=\"android:paddingBottom\">25dp</item>\n  </style>\n  <style name=\"Braze.ContentCards.TextAnnouncement.Title\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_content_cards_title</item>\n    <item name=\"android:textSize\">16.0sp</item>\n    <item name=\"android:textStyle\">bold</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_alignParentTop\">true</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_marginBottom\">10dp</item>\n  </style>\n  <style name=\"Braze.ContentCards.TextAnnouncement.Description\">\n    <item name=\"android:lineSpacingExtra\">1.5dp</item>\n    <item name=\"android:textColor\">@color/com_braze_content_cards_description</item>\n    <item name=\"android:textSize\">13.0sp</item>\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_text_announcement_card_title</item>\n  </style>\n  <style name=\"Braze.ContentCards.ActionHint.TextAnnouncement\">\n    <item name=\"android:includeFontPadding\">false</item>\n    <item name=\"android:layout_alignParentLeft\">true</item>\n    <item name=\"android:layout_below\">@id/com_braze_content_cards_text_announcement_card_description</item>\n    <item name=\"android:layout_marginTop\">12dp</item>\n  </style>\n  <style name=\"Braze.ContentCards.PinnedIcon.TextAnnouncement\"/>\n  <style name=\"Braze.ContentCards.UnreadBar.TextAnnouncement\"/>\n\n  <!-- Push Story -->\n  <style name=\"Braze.Story\"/>\n  <style name=\"Braze.Story.FullLayout\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:weightSum\">1</item>\n  </style>\n  <style name=\"Braze.Story.OneImageLayout\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:clickable\">true</item>\n    <item name=\"android:layout_weight\">1</item>\n    <item name=\"android:maxHeight\">200dp</item>\n  </style>\n  <style name=\"Braze.Story.ImageView\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.Story.ImageView.OneImage\">\n    <item name=\"android:adjustViewBounds\">true</item>\n  </style>\n  <style name=\"Braze.Story.ImageButton\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:background\">#00112233</item>\n    <item name=\"android:paddingTop\">20dp</item>\n    <item name=\"android:paddingBottom\">20dp</item>\n  </style>\n  <style name=\"Braze.Story.ImageButton.OneImage\">\n    <item name=\"android:layout_alignTop\">@+id/com_braze_story_image_view</item>\n    <item name=\"android:layout_alignBottom\">@+id/com_braze_story_image_view</item>\n  </style>\n  <style name=\"Braze.Story.ImageButton.OneImage.Left\">\n    <item name=\"android:src\">@drawable/com_braze_push_ic_left_arrow</item>\n    <item name=\"android:layout_alignLeft\">@+id/com_braze_story_image_view</item>\n    <item name=\"android:paddingRight\">20dp</item>\n    <item name=\"android:paddingLeft\">3dp</item>\n  </style>\n  <style name=\"Braze.Story.ImageButton.OneImage.Right\">\n    <item name=\"android:src\">@drawable/com_braze_push_ic_right_arrow</item>\n    <item name=\"android:layout_alignRight\">@+id/com_braze_story_image_view</item>\n    <item name=\"android:paddingLeft\">20dp</item>\n    <item name=\"android:paddingRight\">3dp</item>\n  </style>\n  <style name=\"Braze.Story.TextViewContainer\">\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:padding\">0dp</item>\n  </style>\n  <style name=\"Braze.Story.TextViewContainer.Title\"/>\n  <style name=\"Braze.Story.TextViewContainer.Title.OneImage\">\n    <item name=\"android:layout_below\">@+id/com_braze_story_image_view</item>\n    <item name=\"android:layout_weight\">0</item>\n  </style>\n  <style name=\"Braze.Story.TextViewContainer.Subtitle\"/>\n  <style name=\"Braze.Story.TextViewContainer.Subtitle.OneImage\">\n    <item name=\"android:layout_below\">@+id/com_braze_story_text_view_container</item>\n    <item name=\"android:layout_weight\">0</item>\n  </style>\n  <style name=\"Braze.Story.TextView\">\n    <item name=\"android:padding\">0dp</item>\n    <item name=\"android:layout_margin\">0dp</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:textSize\">14sp</item>\n    <item name=\"android:fadingEdge\">vertical</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:ellipsize\">end</item>\n  </style>\n  <style name=\"Braze.Story.TextView.Title\">\n    <item name=\"android:textStyle\">normal|bold</item>\n    <item name=\"android:paddingBottom\">5dp</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Story.TextView.Title.OneImage\"/>\n  <style name=\"Braze.Story.TextView.Subtitle\">\n    <item name=\"android:textStyle\">normal</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Story.TextView.Subtitle.OneImage\"/>\n\n  <!-- Inline Image Push -->\n  <style name=\"Braze.Push\"/>\n  <style name=\"Braze.Push.InlineImage\"/>\n  <style name=\"Braze.Push.InlineImage.Layout\">\n    <item name=\"android:orientation\">horizontal</item>\n    <item name=\"android:weightSum\">@dimen/com_braze_push_inline_image_total_weight_sum</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea\"/>\n  <style name=\"Braze.Push.InlineImage.TextArea.Layout\">\n    <item name=\"android:layout_weight\">@dimen/com_braze_push_inline_image_text_area_layout_weight</item>\n    <item name=\"android:layout_width\">0dp</item>\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:layout_marginLeft\">16dp</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header\"/>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.Layout\">\n    <item name=\"android:paddingTop\">16dp</item>\n    <item name=\"android:paddingBottom\">12dp</item>\n    <item name=\"android:layout_marginBottom\">0dp</item>\n    <item name=\"android:gravity\">top</item>\n    <item name=\"android:layout_width\">match_parent</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.AppIcon\">\n    <item name=\"android:layout_width\">18dp</item>\n    <item name=\"android:layout_height\">18dp</item>\n    <item name=\"android:layout_marginRight\">3dp</item>\n    <item name=\"android:contentDescription\">@null</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.AppNameText\" parent=\"@style/TextAppearance.Compat.Notification.Info\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginLeft\">3dp</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_push_inline_image_header_separating_margin</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.TextDivider\" parent=\"@style/TextAppearance.Compat.Notification.Info\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_push_inline_image_header_separating_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_push_inline_image_header_separating_margin</item>\n    <item name=\"android:text\">@string/com_braze_inline_image_push_notification_header_divider_symbol</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.TimeText\" parent=\"@style/TextAppearance.Compat.Notification.Info\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginLeft\">@dimen/com_braze_push_inline_image_header_separating_margin</item>\n    <item name=\"android:layout_marginRight\">@dimen/com_braze_push_inline_image_header_separating_margin</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.TitleContent\"/>\n  <style name=\"Braze.Push.InlineImage.TextArea.TitleContent.Layout\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginRight\">16dp</item>\n    <item name=\"android:orientation\">vertical</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.TitleContent.TitleText\" parent=\"@style/TextAppearance.Compat.Notification.Title\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:ellipsize\">marquee</item>\n    <item name=\"android:fadingEdge\">horizontal</item>\n    <item name=\"android:gravity\">start</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.TextArea.TitleContent.ContentText\" parent=\"@style/TextAppearance.Compat.Notification\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:ellipsize\">marquee</item>\n    <item name=\"android:fadingEdge\">horizontal</item>\n    <item name=\"android:maxLines\">2</item>\n  </style>\n  <style name=\"Braze.Push.InlineImage.Image\"/>\n  <style name=\"Braze.Push.InlineImage.Image.ImageContent\">\n    <item name=\"android:layout_width\">0dp</item>\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:layout_weight\">@dimen/com_braze_push_inline_image_image_layout_weight</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:scaleType\">fitXY</item>\n  </style>\n  <style name=\"Braze.PushTrampoline.Transparent\" parent=\"@android:style/Theme.Translucent.NoTitleBar\">\n    <item name=\"android:windowIsTranslucent\">true</item>\n    <item name=\"android:windowBackground\">@android:color/transparent</item>\n    <item name=\"android:windowContentOverlay\">@null</item>\n    <item name=\"android:windowNoTitle\">true</item>\n    <item name=\"android:windowIsFloating\">true</item>\n    <item name=\"android:backgroundDimEnabled\">false</item>\n  </style>\n\n  <!-- Constrained Inline Image Push -->\n  <style name=\"Braze.Push.InlineImageConstrained\"/>\n  <style name=\"Braze.Push.InlineImageConstrained.Layout\">\n    <item name=\"android:orientation\">horizontal</item>\n    <item name=\"android:weightSum\">@dimen/com_braze_push_inline_image_total_weight_sum</item>\n  </style>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea\"/>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea.Layout\">\n    <item name=\"android:layout_weight\">@dimen/com_braze_push_inline_image_total_weight_sum</item>\n    <item name=\"android:layout_width\">0dp</item>\n    <item name=\"android:layout_height\">match_parent</item>\n    <item name=\"android:orientation\">vertical</item>\n    <item name=\"android:layout_marginLeft\">16dp</item>\n  </style>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea.TitleContent\"/>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea.TitleContent.Layout\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_marginRight\">16dp</item>\n    <item name=\"android:orientation\">vertical</item>\n  </style>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea.TitleContent.TitleText\" parent=\"@style/TextAppearance.Compat.Notification.Title\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:ellipsize\">marquee</item>\n    <item name=\"android:fadingEdge\">horizontal</item>\n    <item name=\"android:gravity\">start</item>\n    <item name=\"android:singleLine\">true</item>\n  </style>\n  <style name=\"Braze.Push.InlineImageConstrained.TextArea.TitleContent.ContentText\" parent=\"@style/TextAppearance.Compat.Notification\">\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:ellipsize\">marquee</item>\n    <item name=\"android:fadingEdge\">horizontal</item>\n    <item name=\"android:maxLines\">2</item>\n    <item name=\"android:textSize\">12sp</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">ليس لدينا أي تحديث. يرجى التحقق مرة أخرى لاحقاً.</string>\n  <string name=\"com_braze_feed_connection_error_title\">خلل في الاتصال</string>\n  <string name=\"com_braze_feed_connection_error_body\">لا يمكن إجراء الاتصال بالشبكة. يرجى تكرار المحاولة لاحقا. </string>\n  <string name=\"com_braze_inappmessage_close_content_description\">لإغلاق</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Nemáme žádné aktualizace. Zkontrolujte prosím znovu později.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Chyba připojení</string>\n  <string name=\"com_braze_feed_connection_error_body\">Nelze navázat síťové připojení. Prosím zkuste to znovu později.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">zavřít</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Vi har ingen updates. Prøv venligst senere.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Netværksfejl</string>\n  <string name=\"com_braze_feed_connection_error_body\">Kan ikke etablere netværksforbindelse. Prøv venligst senere.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">at lukke</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Derzeit sind keine Updates verfügbar. Bitte später noch einmal versuchen.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Verbindungsfehler</string>\n  <string name=\"com_braze_feed_connection_error_body\">Netzwerkverbindung kann nicht aufgebaut werden. Bitte später noch einmal versuchen.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">schließen</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">No tenemos ninguna actualización. Vuelva a verificar más tarde.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Error de conexión</string>\n  <string name=\"com_braze_feed_connection_error_body\">No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">cerrar</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-es-rES/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">No tenemos actualizaciones. Por favor compruébelo más tarde.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Error de conexión</string>\n  <string name=\"com_braze_feed_connection_error_body\">No se puede establecer conexión de red. Por favor inténtelo más tarde.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">cerrar</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Uuendusi pole praegu saadaval. Proovige hiljem uuesti.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Üheduse viga</string>\n  <string name=\"com_braze_feed_connection_error_body\">Võrguühenduse loomine ebaõnnestus. Proovige hiljem uuesti.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">sulgema</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Päivityksiä ei ole saatavilla. Tarkista myöhemmin uudelleen.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Yhteysvirhe</string>\n  <string name=\"com_braze_feed_connection_error_body\">Verkkoyhteyttä ei voida luoda. Yritä myöhemmin uudelleen.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">sulkea</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Aucune mise à jour disponible. Veuillez vérifier ultérieurement.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Erreur de connexion.</string>\n  <string name=\"com_braze_feed_connection_error_body\">Impossible d\\'établir la connexion réseau. Veuillez réessayer ultérieurement.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">fermer</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-hdpi/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_card_background_border_left\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_right\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_top\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_bottom\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_shadow_bottom\">1.0dp</dimen>\n\n  <dimen name=\"com_braze_content_card_background_border_left\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_right\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_top\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_bottom\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_bottom\">1.0dp</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।</string>\n  <string name=\"com_braze_feed_connection_error_title\">कनेक्शन की त्रुटि</string>\n  <string name=\"com_braze_feed_connection_error_body\">नेटवर्क कनेक्शन स्थापित नहीं हो रहा है। कृपया बाद में दोबारा प्रयास करें।.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">बंद करना</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Kami tidak memiliki pembaruan. Coba lagi nanti.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Kesalahan Koneksi</string>\n  <string name=\"com_braze_feed_connection_error_body\">Tidak bisa melakukan koneksi jaringan. Coba lagi nanti.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">untuk menutup</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Non ci sono aggiornamenti. Ricontrollare più tardi.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Errore di connessione</string>\n  <string name=\"com_braze_feed_connection_error_body\">Impossibile stabilire una connessione di rete. Riprovare più tardi.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">chiudere</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">אין לנו עדכונים. בבקשה בדוק שוב בקרוב.</string>\n  <string name=\"com_braze_feed_connection_error_title\">שגיאת חיבור רשת</string>\n  <string name=\"com_braze_feed_connection_error_body\">לא ניתן לקבוע חיבור רשת. בבקשה נסה שוב בקרוב.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">לסגור</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">アップデートはありません。後でもう一度確認してください。</string>\n  <string name=\"com_braze_feed_connection_error_title\">接続エラー</string>\n  <string name=\"com_braze_feed_connection_error_body\">ネットワークに接続できません。後でもう一度試してください。</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">閉じる</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-km/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ.</string>\n  <string name=\"com_braze_feed_connection_error_title\">កំហុសឆ្គងក្នុងការតភ្ជាប់</string>\n  <string name=\"com_braze_feed_connection_error_body\">មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">បិទ</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">업데이트가 없습니다. 다음에 다시 확인해 주십시오.</string>\n  <string name=\"com_braze_feed_connection_error_title\">연결 오류</string>\n  <string name=\"com_braze_feed_connection_error_body\">네트워크 연결을 할 수 없습니다. 나중에 다시 시도해 주십시오.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">가까운</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-lo/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ.</string>\n  <string name=\"com_braze_feed_connection_error_title\">ການ​ເຊື່ອມ​ຕໍ່​ຜິດ​ພາດ</string>\n  <string name=\"com_braze_feed_connection_error_body\">ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">ໃກ້</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-mdpi/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_card_background_border_left\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_right\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_top\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_bottom\">1.5dp</dimen>\n  <dimen name=\"com_braze_card_background_shadow_bottom\">1.0dp</dimen>\n\n  <dimen name=\"com_braze_content_card_background_border_left\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_right\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_top\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_bottom\">1.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_bottom\">1.0dp</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-ms/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Tiada kemas kini. Sila periksa kemudian.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Ralat Sambungan</string>\n  <string name=\"com_braze_feed_connection_error_body\">Tidak boleh membuat sambungan rangkaian. Sila cuba kemudian.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">untuk menutup</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-my/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ .</string>\n  <string name=\"com_braze_feed_connection_error_title\">ဆက္သြယ္ေရး အမွား</string>\n  <string name=\"com_braze_feed_connection_error_body\">ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။ ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">ပိတ်ရန်</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-nb/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Vi har ingen oppdateringer. Vennligst sjekk igjen senere.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Tilkoblingsfeil</string>\n  <string name=\"com_braze_feed_connection_error_body\">Kan ikke etablere nettverkstilkobling. Vennligst prøv igjen senere.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">å lukke</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"com_braze_content_card_background_border\">#555555</color>\n  <color name=\"com_braze_content_card_background_shadow\">#555555</color>\n  <color name=\"com_braze_content_card_background\">#555555</color>\n\n  <!-- The color used to highlight unread Content Cards at their bottom edge -->\n  <color name=\"com_braze_content_cards_unread_bar_color\">#8AC7FF</color>\n\n  <!-- The color of the \"Read More >\" text when card actions are present -->\n  <color name=\"com_braze_content_cards_action_hint_text_color\">#8AC7FF</color>\n\n  <color name=\"com_braze_content_cards_title\">@android:color/white</color>\n  <color name=\"com_braze_content_cards_description\">@android:color/white</color>\n  <color name=\"com_braze_content_cards_display_background_color\">#1E1E1E</color>\n\n  <color name=\"com_braze_content_card_empty_text_color\">@android:color/white</color>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-night/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_content_card_background_border_left\">0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_right\">0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_top\">0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_bottom\">0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_bottom\">0dp</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"Braze.Push.InlineImage.TextArea.Header.AppIcon\">\n    <item name=\"android:layout_width\">18dp</item>\n    <item name=\"android:layout_height\">18dp</item>\n    <item name=\"android:layout_marginRight\">3dp</item>\n    <item name=\"android:contentDescription\">@null</item>\n    <item name=\"android:tint\">#777777</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Er zijn geen updates. Probeer het later opnieuw.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Verbindingsfout</string>\n  <string name=\"com_braze_feed_connection_error_body\">Kan geen netwerkverbinding maken. Probeer het later opnieuw.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">sluiten</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Brak aktualizacji. Proszę sprawdzić ponownie później.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Błąd połączenia</string>\n  <string name=\"com_braze_feed_connection_error_body\">Nie można ustanowić połączenia z siecią. Proszę spróbować ponownie później.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">zamknąć</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Não temos nenhuma atualização. Verifique novamente mais tarde.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Erro de conexão</string>\n  <string name=\"com_braze_feed_connection_error_body\">Não é possível estabelecer uma conexão de rede. Tente novamente mais tarde.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">fechar</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-pt-rPT/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Não temos atualizações. Por favor, verifique mais tarde.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Erro de Ligação</string>\n  <string name=\"com_braze_feed_connection_error_body\">Não é possível estabelecer a ligação à rede. Por favor, tente mais tarde.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">fechar</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Обновления недоступны. Пожалуйста, проверьте снова позже.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Ошибка подключения</string>\n  <string name=\"com_braze_feed_connection_error_body\">Невозможно установить сетевое подключение. Пожалуйста, повторите попытку позже.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">слишком близко</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-sv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Det finns inga uppdateringar. Försök igen senare.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Anslutningsfel</string>\n  <string name=\"com_braze_feed_connection_error_body\">Det gick inte att skapa en nätverksanslutning. Försök igen senare.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">för nära</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-sw600dp/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_feed_max_width\">600.0dp</dimen>\n  <dimen name=\"com_braze_content_cards_max_width\">600.0dp</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-sw600dp/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- In-App Message Full -->\n  <style name=\"Braze.InAppMessage.Full\">\n    <item name=\"android:layout_height\">720dp</item>\n    <item name=\"android:layout_width\">450dp</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:background\">@color/com_braze_inappmessage_background_light</item>\n    <item name=\"android:padding\">0.0dp</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง.</string>\n  <string name=\"com_braze_feed_connection_error_title\">ผิดพลาดการเชื่อมต่อ</string>\n  <string name=\"com_braze_feed_connection_error_body\">ไม่สามารถสร้างการเชื่อมต่อเครือข่าย กรุณาลองใหม่ภายหลัง.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">ใกล้</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-tl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Wala kaming mga update. Mangyaring suriin muli sa ibang pagkakataon.</string>\n  <string name=\"com_braze_feed_connection_error_title\">May Error sa Koneksyon</string>\n  <string name=\"com_braze_feed_connection_error_body\">Hindi makapagtatag ng koneksyon sa network. Mangyaring subukan muli mamaya.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">Isara</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Оновлення недоступні. Будь ласка, перевірте знову пізніше.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Помилка підключення</string>\n  <string name=\"com_braze_feed_connection_error_body\">неможливо встановити з\\'єднання з мережею. Будь ласка, спробуйте ще раз пізніше.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">закрити</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-v21/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"Braze.Story.TextView\"\n         parent=\"@android:style/TextAppearance.Material.Notification\">\n    <item name=\"android:padding\">0dp</item>\n    <item name=\"android:layout_margin\">0dp</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:textSize\">14sp</item>\n    <item name=\"android:fadingEdge\">vertical</item>\n    <item name=\"android:gravity\">center</item>\n    <item name=\"android:ellipsize\">end</item>\n    <item name=\"android:maxLines\">2</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Slideup.Container\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">@dimen/com_braze_inappmessage_slideup_max_width</item>\n    <item name=\"android:layout_marginLeft\">10dp</item>\n    <item name=\"android:layout_marginRight\">10dp</item>\n    <item name=\"android:layout_marginBottom\">10dp</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_slideup_background</item>\n    <!-- The value for the elevation is 1dp fewer than the margin -->\n    <item name=\"android:elevation\">9dp</item>\n  </style>\n  <style name=\"Braze.InAppMessage.Modal\">\n    <item name=\"android:layout_height\">wrap_content</item>\n    <item name=\"android:layout_width\">wrap_content</item>\n    <item name=\"android:paddingTop\">0.0dp</item>\n    <item name=\"android:paddingRight\">0.0dp</item>\n    <item name=\"android:paddingLeft\">0.0dp</item>\n    <item name=\"android:paddingBottom\">20.0dp</item>\n    <item name=\"android:layout_centerInParent\">true</item>\n    <item name=\"android:background\">@drawable/com_braze_inappmessage_modal_background</item>\n    <item name=\"android:layout_margin\">15dp</item>\n    <item name=\"inAppMessageBoundedLayoutMinWidth\">@dimen/com_braze_inappmessage_modal_min_width</item>\n    <item name=\"inAppMessageBoundedLayoutMaxWidth\">@dimen/com_braze_inappmessage_modal_max_width</item>\n    <item name=\"inAppMessageBoundedLayoutMaxHeight\">@dimen/com_braze_inappmessage_modal_max_height</item>\n    <!-- The value for the elevation is 1dp fewer than the margin -->\n    <item name=\"android:elevation\">14dp</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">Chúng tôi không có cập nhật nào. Vui lòng kiểm tra lại sau.</string>\n  <string name=\"com_braze_feed_connection_error_title\">Lỗi Kết Nối</string>\n  <string name=\"com_braze_feed_connection_error_body\">Không thể thiết lập kết nối mạng. Vui lòng thử lại sau.</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">đóng</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-xhdpi/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <dimen name=\"com_braze_card_background_border_left\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_right\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_border_top\">0.5dp</dimen>\n  <dimen name=\"com_braze_card_background_border_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_card_background_shadow_bottom\">1.0dp</dimen>\n\n  <dimen name=\"com_braze_content_card_background_border_left\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_right\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_top\">0.5dp</dimen>\n  <dimen name=\"com_braze_content_card_background_border_bottom\">1.0dp</dimen>\n  <dimen name=\"com_braze_content_card_background_shadow_bottom\">1.0dp</dimen>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">暂时没有更新。\\n请稍候再试。</string>\n  <string name=\"com_braze_feed_connection_error_title\">连接错误</string>\n  <string name=\"com_braze_feed_connection_error_body\">无法建立网络连接。\\n请稍候再试。</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">接近</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">暫時沒有更新。\\n請稍候再試。</string>\n  <string name=\"com_braze_feed_connection_error_title\">連線錯誤</string>\n  <string name=\"com_braze_feed_connection_error_body\">無法建立網路連線。\\n請稍候再試。</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">接近</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-zh-rSG/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">暂时没有更新。\\n请稍候再试。</string>\n  <string name=\"com_braze_feed_connection_error_title\">连接错误</string>\n  <string name=\"com_braze_feed_connection_error_body\">无法建立网络连接。\\n请稍候再试。</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">接近</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-ui/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- News feed -->\n  <string name=\"com_braze_feed_empty\">暫時沒有更新。\\n請稍候再試。</string>\n  <string name=\"com_braze_feed_connection_error_title\">連線錯誤</string>\n  <string name=\"com_braze_feed_connection_error_body\">無法建立網路連線。\\n請稍候再試。</string>\n  <string name=\"com_braze_inappmessage_close_content_description\">接近</string>\n</resources>\n"
  },
  {
    "path": "android-sdk-unity/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\ndependencies {\n  compileOnly files(\"libs/Prime31UnityActivity-1.0.jar\")\n  compileOnly files(\"libs/unity-2019.2.12f1.jar\")\n  compileOnly \"androidx.annotation:annotation:${ANDROIDX_ANNOTATIONS_VERSION}\"\n  implementation \"org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}\"\n  implementation project(':android-sdk-ui')\n}\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.braze.unity\">\n  <application/>\n</manifest>\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/BrazeUnityActivityWrapper.kt",
    "content": "package com.braze.unity\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.unity.configuration.UnityConfigurationProvider\nimport com.braze.unity.enums.UnityInAppMessageManagerAction\nimport com.braze.unity.utils.MessagingUtils\nimport com.braze.unity.utils.MessagingUtils.BrazeInternalComponentMethod\nimport com.braze.Braze\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.Priority.V\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.activities.ContentCardsActivity\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.InAppMessageOperation\nimport com.braze.ui.inappmessage.listeners.DefaultHtmlInAppMessageActionListener\nimport com.braze.ui.inappmessage.listeners.DefaultInAppMessageManagerListener\nimport org.json.JSONArray\n\n/**\n * This class allows UnityPlayerNativeActivity and UnityPlayerActivity instances to\n * integrate Braze by calling appropriate methods during each phase of the Android [Activity] lifecycle.\n */\nclass BrazeUnityActivityWrapper {\n    private lateinit var unityConfigurationProvider: UnityConfigurationProvider\n    private var nextInAppMessageDisplayOperation = InAppMessageOperation.DISPLAY_NOW\n    private var wasInAppMessageDisplayRequested = false\n\n    /**\n     * Call from [Activity.onCreate].\n     */\n    fun onCreateCalled(activity: Activity) {\n        val applicationContext = activity.applicationContext\n        val braze = Braze.getInstance(applicationContext)\n        val config = getUnityConfigurationProvider(applicationContext)\n        braze.subscribeToNewInAppMessages(\n            EventSubscriberFactory.createInAppMessageEventSubscriber(\n                config\n            )\n        )\n        braze.subscribeToFeedUpdates(EventSubscriberFactory.createFeedUpdatedEventSubscriber(config))\n        braze.subscribeToContentCardsUpdates(\n            EventSubscriberFactory.createContentCardsEventSubscriber(\n                config\n            )\n        )\n        braze.subscribeToFeatureFlagsUpdates(\n            EventSubscriberFactory.createFeatureFlagsEventSubscriber(\n                config\n            )\n        )\n        braze.subscribeToPushNotificationEvents(\n            EventSubscriberFactory.createPushEventSubscriber(\n                config\n            )\n        )\n        braze.subscribeToSdkAuthenticationFailures(\n            EventSubscriberFactory.createSdkAuthenticationFailureSubscriber(config)\n        )\n        if (config.autoSetInAppMessageManagerListener) {\n            brazelog(I) { \"Automatically setting In App Message Manager listener in BrazeUnityActivityWrapper.\" }\n            setInAppMessageListener()\n        }\n        nextInAppMessageDisplayOperation = config.initialInAppMessageDisplayOperation\n        brazelog(I) { \"Finished onCreateCalled setup\" }\n    }\n\n    /**\n     * Call from [Activity.onStart].\n     */\n    fun onStartCalled(activity: Activity?) {\n        if (activity != null) {\n            Braze.getInstance(activity).openSession(activity)\n        }\n    }\n\n    /**\n     * Call from [Activity.onResume].\n     */\n    fun onResumeCalled(activity: Activity) {\n        if (getUnityConfigurationProvider(activity).showInAppMessagesAutomaticallyKey) {\n            BrazeInAppMessageManager.getInstance().registerInAppMessageManager(activity)\n        }\n    }\n\n    /**\n     * Call from [Activity.onPause].\n     */\n    fun onPauseCalled(activity: Activity) {\n        if (getUnityConfigurationProvider(activity).showInAppMessagesAutomaticallyKey) {\n            BrazeInAppMessageManager.getInstance().unregisterInAppMessageManager(activity)\n        }\n    }\n\n    /**\n     * Call from [Activity.onStop].\n     */\n    fun onStopCalled(activity: Activity?) {\n        if (activity != null) {\n            Braze.getInstance(activity).closeSession(activity)\n        }\n    }\n\n    /**\n     * Call from [Activity.onNewIntent].\n     */\n    fun onNewIntentCalled(intent: Intent?, activity: Activity) {\n        // If the Activity is already open and we receive an intent to open the Activity again, we set\n        // the new intent as the current one (which has the new intent extras).\n        activity.intent = intent\n    }\n\n    fun onNewUnityInAppMessageManagerAction(actionEnumValue: Int) {\n        when (val action = UnityInAppMessageManagerAction.getTypeFromValue(actionEnumValue)) {\n            UnityInAppMessageManagerAction.IAM_DISPLAY_NOW,\n            UnityInAppMessageManagerAction.IAM_DISPLAY_LATER,\n            UnityInAppMessageManagerAction.IAM_DISCARD ->\n                action.inAppMessageOperation?.let { nextInAppMessageDisplayOperation = it }\n            else -> {\n                brazelog(V) { \"Failed to map unity IAM manager action value: $action\" }\n                // Do nothing\n            }\n        }\n    }\n\n    fun requestInAppMessageDisplay() {\n        wasInAppMessageDisplayRequested = true\n        BrazeInAppMessageManager.getInstance().requestDisplayInAppMessage()\n    }\n\n    fun launchContentCardsActivity(activity: Activity) {\n        activity.startActivity(Intent(activity, ContentCardsActivity::class.java))\n    }\n\n    fun setInAppMessageListener() {\n        brazelog(I) { \"Setting in app message manager custom listener.\" }\n        BrazeInAppMessageManager.getInstance().setCustomInAppMessageManagerListener(\n            object : DefaultInAppMessageManagerListener() {\n                override fun beforeInAppMessageDisplayed(inAppMessage: IInAppMessage): InAppMessageOperation {\n                    super.beforeInAppMessageDisplayed(inAppMessage)\n                    MessagingUtils.sendToBrazeInternalComponent(\n                        BrazeInternalComponentMethod.BEFORE_IAM_DISPLAYED,\n                        inAppMessage.forJsonPut().toString()\n                    )\n                    // If this was requested, override whatever was set previously\n                    return if (wasInAppMessageDisplayRequested) {\n                        brazelog {\n                            \"In App Message display requested, not using \" +\n                                \"previously set value of $nextInAppMessageDisplayOperation\"\n                        }\n                        wasInAppMessageDisplayRequested = false\n                        InAppMessageOperation.DISPLAY_NOW\n                    } else {\n                        nextInAppMessageDisplayOperation\n                    }\n                }\n\n                override fun onInAppMessageDismissed(inAppMessage: IInAppMessage) {\n                    super.onInAppMessageDismissed(inAppMessage)\n                    MessagingUtils.sendToBrazeInternalComponent(\n                        BrazeInternalComponentMethod.ON_IAM_DISMISSED,\n                        inAppMessage.forJsonPut().toString()\n                    )\n                }\n\n                override fun onInAppMessageClicked(\n                    inAppMessage: IInAppMessage,\n                ): Boolean {\n                    MessagingUtils.sendToBrazeInternalComponent(\n                        BrazeInternalComponentMethod.ON_IAM_CLICKED,\n                        inAppMessage.forJsonPut().toString()\n                    )\n                    return super.onInAppMessageClicked(inAppMessage)\n                }\n\n                override fun onInAppMessageButtonClicked(\n                    inAppMessage: IInAppMessage,\n                    button: MessageButton\n                ): Boolean {\n                    val jsonArray = JSONArray()\n                        .put(inAppMessage.forJsonPut())\n                        .put(button.forJsonPut())\n                    MessagingUtils.sendToBrazeInternalComponent(\n                        BrazeInternalComponentMethod.ON_IAM_BUTTON_CLICKED,\n                        jsonArray.toString()\n                    )\n                    return super.onInAppMessageButtonClicked(inAppMessage, button)\n                }\n            }\n        )\n\n        BrazeInAppMessageManager.getInstance().setCustomHtmlInAppMessageActionListener(\n            object : DefaultHtmlInAppMessageActionListener() {\n                override fun onOtherUrlAction(\n                    inAppMessage: IInAppMessage,\n                    url: String,\n                    queryBundle: Bundle\n                ): Boolean {\n                    val jsonArray = JSONArray()\n                        .put(inAppMessage.forJsonPut())\n                        .put(url)\n                    MessagingUtils.sendToBrazeInternalComponent(\n                        BrazeInternalComponentMethod.ON_IAM_HTML_CLICKED,\n                        jsonArray.toString()\n                    )\n                    return super.onOtherUrlAction(inAppMessage, url, queryBundle)\n                }\n            }\n        )\n    }\n\n    private fun getUnityConfigurationProvider(context: Context): UnityConfigurationProvider {\n        if (!this::unityConfigurationProvider.isInitialized) {\n            unityConfigurationProvider = UnityConfigurationProvider(context.applicationContext)\n        }\n        return unityConfigurationProvider\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/BrazeUnityPlayerActivity.kt",
    "content": "package com.braze.unity\n\nimport android.content.Intent\nimport android.os.Bundle\nimport com.unity3d.player.UnityPlayerActivity\n\n/**\n * This is a wrapper subclass of the [com.unity3d.player.UnityPlayerActivity] class. It calls the necessary Braze methods\n * to ensure that analytics are collected and that push notifications are properly forwarded to\n * the Unity application.\n *\n * NOTE: This Activity is not compatible with Prime31 plugins. If you are using any Prime31 plugins, you\n * must use the [BrazeUnityPlayerActivity] in the com.braze.unity.prime31compatible package instead.\n */\nopen class BrazeUnityPlayerActivity : UnityPlayerActivity() {\n    private lateinit var unityActivityWrapper: BrazeUnityActivityWrapper\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        unityActivityWrapper = BrazeUnityActivityWrapper()\n        unityActivityWrapper.onCreateCalled(this)\n    }\n\n    override fun onStart() {\n        super.onStart()\n        unityActivityWrapper.onStartCalled(this)\n    }\n\n    public override fun onResume() {\n        super.onResume()\n        unityActivityWrapper.onResumeCalled(this)\n    }\n\n    public override fun onPause() {\n        unityActivityWrapper.onPauseCalled(this)\n        super.onPause()\n    }\n\n    override fun onStop() {\n        unityActivityWrapper.onStopCalled(this)\n        super.onStop()\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        unityActivityWrapper.onNewIntentCalled(intent, this)\n    }\n\n    @JvmName(\"onNewUnityInAppMessageManagerAction\")\n    fun onNewUnityInAppMessageManagerAction(actionEnumValue: Int) {\n        unityActivityWrapper.onNewUnityInAppMessageManagerAction(actionEnumValue)\n    }\n\n    @JvmName(\"launchContentCardsActivity\")\n    fun launchContentCardsActivity() {\n        unityActivityWrapper.launchContentCardsActivity(this)\n    }\n\n    @JvmName(\"setInAppMessageListener\")\n    fun setInAppMessageListener() {\n        unityActivityWrapper.setInAppMessageListener()\n    }\n\n    @JvmName(\"requestDisplayInAppMessage\")\n    fun requestDisplayInAppMessage() {\n        unityActivityWrapper.requestInAppMessageDisplay()\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/EventSubscriberFactory.kt",
    "content": "package com.braze.unity\n\nimport com.braze.events.FeedUpdatedEvent\nimport com.braze.unity.configuration.UnityConfigurationProvider\nimport com.braze.enums.BrazePushEventType\nimport com.braze.events.BrazePushEvent\nimport com.braze.events.BrazeSdkAuthenticationErrorEvent\nimport com.braze.events.ContentCardsUpdatedEvent\nimport com.braze.events.FeatureFlagsUpdatedEvent\nimport com.braze.events.IEventSubscriber\nimport com.braze.events.InAppMessageEvent\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport com.braze.unity.utils.MessagingUtils.sendContentCardsUpdatedEventToUnity\nimport com.braze.unity.utils.MessagingUtils.sendFeatureFlagsUpdatedEventToUnity\nimport com.braze.unity.utils.MessagingUtils.sendFeedUpdatedEventToUnity\nimport com.braze.unity.utils.MessagingUtils.sendInAppMessageReceivedMessage\nimport com.braze.unity.utils.MessagingUtils.sendPushEventToUnity\nimport com.braze.unity.utils.MessagingUtils.sendSdkAuthErrorEventToUnity\n\nobject EventSubscriberFactory {\n    private val TAG = getBrazeLogTag(EventSubscriberFactory::class.java)\n\n    fun createInAppMessageEventSubscriber(config: UnityConfigurationProvider): IEventSubscriber<InAppMessageEvent> {\n        return IEventSubscriber { inAppMessageEvent: InAppMessageEvent ->\n            val isInAppMessageEventSent =\n                sendInAppMessageReceivedMessage(\n                    config.inAppMessageListenerGameObjectName,\n                    config.inAppMessageListenerCallbackMethodName,\n                    inAppMessageEvent.inAppMessage\n                )\n            brazelog(TAG) { \"Did send in-app message event to Unity Player?: $isInAppMessageEventSent\" }\n        }\n    }\n\n    fun createFeedUpdatedEventSubscriber(config: UnityConfigurationProvider): IEventSubscriber<FeedUpdatedEvent> {\n        return IEventSubscriber { feedUpdatedEvent: FeedUpdatedEvent ->\n            val isFeedUpdatedEventSent = sendFeedUpdatedEventToUnity(\n                config.feedListenerGameObjectName,\n                config.feedListenerCallbackMethodName,\n                feedUpdatedEvent\n            )\n            brazelog(TAG) { \"Did send Feed updated event to Unity Player?: $isFeedUpdatedEventSent\" }\n        }\n    }\n\n    fun createContentCardsEventSubscriber(config: UnityConfigurationProvider): IEventSubscriber<ContentCardsUpdatedEvent> {\n        return IEventSubscriber { contentCardsUpdatedEvent: ContentCardsUpdatedEvent ->\n            val isContentCardsEventSent =\n                sendContentCardsUpdatedEventToUnity(\n                    config.contentCardsUpdatedListenerGameObjectName,\n                    config.contentCardsUpdatedListenerCallbackMethodName,\n                    contentCardsUpdatedEvent\n                )\n            brazelog(TAG) { \"Did send Content Cards updated event to Unity Player?: $isContentCardsEventSent\" }\n        }\n    }\n\n    fun createFeatureFlagsEventSubscriber(config: UnityConfigurationProvider): IEventSubscriber<FeatureFlagsUpdatedEvent> {\n        return IEventSubscriber { featureFlagsUpdatedEvent: FeatureFlagsUpdatedEvent ->\n            val isFeatureFlagUpdatedEventSent =\n                sendFeatureFlagsUpdatedEventToUnity(\n                    config.featureFlagsUpdatedListenerGameObjectName,\n                    config.featureFlagsUpdatedListenerCallbackMethodName,\n                    featureFlagsUpdatedEvent\n                )\n            brazelog(TAG) { \"Did send Content Cards updated event to Unity Player?: $isFeatureFlagUpdatedEventSent\" }\n        }\n    }\n\n    fun createSdkAuthenticationFailureSubscriber(config: UnityConfigurationProvider): IEventSubscriber<BrazeSdkAuthenticationErrorEvent> {\n        return IEventSubscriber { sdkAuthErrorEvent: BrazeSdkAuthenticationErrorEvent ->\n            val isSdkAuthErrorSent =\n                sendSdkAuthErrorEventToUnity(\n                    config.sdkAuthenticationFailureListenerGameObjectName,\n                    config.sdkAuthenticationFailureListenerCallbackMethodName,\n                    sdkAuthErrorEvent\n                )\n            brazelog { \"Did send SDK Authentication failure event to Unity Player?: $isSdkAuthErrorSent\" }\n        }\n    }\n\n    fun createPushEventSubscriber(config: UnityConfigurationProvider): IEventSubscriber<BrazePushEvent> {\n        return IEventSubscriber { event: BrazePushEvent ->\n            val (callback, gameObject) = when (event.eventType) {\n                BrazePushEventType.NOTIFICATION_RECEIVED -> Pair(config.pushReceivedCallbackMethodName, config.pushReceivedGameObjectName)\n                BrazePushEventType.NOTIFICATION_DELETED -> Pair(config.pushDeletedCallbackMethodName, config.pushDeletedGameObjectName)\n                BrazePushEventType.NOTIFICATION_OPENED -> Pair(config.pushOpenedCallbackMethodName, config.pushOpenedGameObjectName)\n                else -> return@IEventSubscriber\n            }\n            val wasMessageSent = sendPushEventToUnity(gameObject, callback, event)\n            brazelog { \"Did send Braze Push event to Unity Player?: $wasMessageSent \\nEvent: $event\" }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/configuration/UnityConfigurationProvider.kt",
    "content": "package com.braze.unity.configuration\n\nimport android.content.Context\nimport com.braze.unity.enums.UnityMessageType\nimport com.braze.configuration.CachedConfigurationProvider\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.inappmessage.InAppMessageOperation\nimport com.braze.ui.inappmessage.InAppMessageOperation.Companion.fromValue\n\nclass UnityConfigurationProvider(context: Context) : CachedConfigurationProvider(\n    context, false\n) {\n    val inAppMessageListenerGameObjectName: String?\n        get() = getStringValue(INAPP_LISTENER_GAME_OBJECT_NAME_KEY, null)\n    val inAppMessageListenerCallbackMethodName: String?\n        get() = getStringValue(INAPP_LISTENER_CALLBACK_METHOD_NAME_KEY, null)\n    val feedListenerGameObjectName: String?\n        get() = getStringValue(FEED_LISTENER_GAME_OBJECT_NAME_KEY, null)\n    val feedListenerCallbackMethodName: String?\n        get() = getStringValue(FEED_LISTENER_CALLBACK_METHOD_NAME_KEY, null)\n    @Suppress(\"BooleanPropertyNaming\")\n    val showInAppMessagesAutomaticallyKey: Boolean\n        get() = getBooleanValue(INAPP_SHOW_INAPP_MESSAGES_AUTOMATICALLY_KEY, true)\n    val pushReceivedGameObjectName: String?\n        get() = getStringValue(PUSH_RECEIVED_GAME_OBJECT_NAME_KEY, null)\n    val pushReceivedCallbackMethodName: String?\n        get() = getStringValue(PUSH_RECEIVED_CALLBACK_METHOD_NAME_KEY, null)\n    val pushOpenedGameObjectName: String?\n        get() = getStringValue(PUSH_OPENED_GAME_OBJECT_NAME_KEY, null)\n    val pushOpenedCallbackMethodName: String?\n        get() = getStringValue(PUSH_OPENED_CALLBACK_METHOD_NAME_KEY, null)\n    val pushDeletedGameObjectName: String?\n        get() = getStringValue(PUSH_DELETED_GAME_OBJECT_NAME_KEY, null)\n    val pushDeletedCallbackMethodName: String?\n        get() = getStringValue(PUSH_DELETED_CALLBACK_METHOD_NAME_KEY, null)\n    val contentCardsUpdatedListenerGameObjectName: String?\n        get() = getStringValue(CONTENT_CARDS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY, null)\n    val contentCardsUpdatedListenerCallbackMethodName: String?\n        get() = getStringValue(CONTENT_CARDS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY, null)\n    val featureFlagsUpdatedListenerGameObjectName: String?\n        get() = getStringValue(FEATURE_FLAGS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY, null)\n    val featureFlagsUpdatedListenerCallbackMethodName: String?\n        get() = getStringValue(FEATURE_FLAGS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY, null)\n    val sdkAuthenticationFailureListenerGameObjectName: String?\n        get() = getStringValue(SDK_AUTHENTICATION_FAILURE_LISTENER_GAME_OBJECT_NAME_KEY, null)\n    val sdkAuthenticationFailureListenerCallbackMethodName: String?\n        get() = getStringValue(SDK_AUTHENTICATION_FAILURE_LISTENER_CALLBACK_METHOD_NAME_KEY, null)\n    @Suppress(\"BooleanPropertyNaming\")\n    val autoSetInAppMessageManagerListener: Boolean\n        get() = getBooleanValue(INAPP_AUTO_SET_MANAGER_LISTENER_KEY, true)\n    val initialInAppMessageDisplayOperation: InAppMessageOperation\n        get() {\n            val rawValue = getStringValue(INAPP_INITIAL_DISPLAY_OPERATION_KEY, null)\n            val operation = fromValue(rawValue)\n            return operation ?: InAppMessageOperation.DISPLAY_NOW\n        }\n\n    @Suppress(\"LongMethod\")\n    fun configureListener(messageTypeValue: Int, gameObject: String, methodName: String) {\n        val messageType = UnityMessageType.getTypeFromValue(messageTypeValue)\n        if (messageType == null) {\n            brazelog {\n                \"Got bad message type $messageTypeValue. Cannot configure a listener on object \" +\n                    \"$gameObject for method $methodName\"\n            }\n            return\n        }\n        when (messageType) {\n            UnityMessageType.PUSH_PERMISSIONS_PROMPT_RESPONSE,\n            UnityMessageType.PUSH_TOKEN_RECEIVED_FROM_SYSTEM -> {}\n            UnityMessageType.PUSH_RECEIVED -> {\n                putStringIntoRuntimeConfiguration(PUSH_RECEIVED_GAME_OBJECT_NAME_KEY, gameObject)\n                putStringIntoRuntimeConfiguration(\n                    PUSH_RECEIVED_CALLBACK_METHOD_NAME_KEY,\n                    methodName\n                )\n            }\n            UnityMessageType.PUSH_OPENED -> {\n                putStringIntoRuntimeConfiguration(PUSH_OPENED_GAME_OBJECT_NAME_KEY, gameObject)\n                putStringIntoRuntimeConfiguration(PUSH_OPENED_CALLBACK_METHOD_NAME_KEY, methodName)\n            }\n            UnityMessageType.PUSH_DELETED -> {\n                putStringIntoRuntimeConfiguration(PUSH_DELETED_GAME_OBJECT_NAME_KEY, gameObject)\n                putStringIntoRuntimeConfiguration(PUSH_DELETED_CALLBACK_METHOD_NAME_KEY, methodName)\n            }\n            UnityMessageType.IN_APP_MESSAGE -> {\n                putStringIntoRuntimeConfiguration(INAPP_LISTENER_GAME_OBJECT_NAME_KEY, gameObject)\n                putStringIntoRuntimeConfiguration(\n                    INAPP_LISTENER_CALLBACK_METHOD_NAME_KEY,\n                    methodName\n                )\n            }\n            UnityMessageType.NEWS_FEED -> {\n                putStringIntoRuntimeConfiguration(FEED_LISTENER_GAME_OBJECT_NAME_KEY, gameObject)\n                putStringIntoRuntimeConfiguration(\n                    FEED_LISTENER_CALLBACK_METHOD_NAME_KEY,\n                    methodName\n                )\n            }\n            UnityMessageType.CONTENT_CARDS_UPDATED -> {\n                putStringIntoRuntimeConfiguration(\n                    CONTENT_CARDS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY, gameObject\n                )\n                putStringIntoRuntimeConfiguration(\n                    CONTENT_CARDS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY, methodName\n                )\n            }\n            UnityMessageType.SDK_AUTHENTICATION_FAILURE -> {\n                putStringIntoRuntimeConfiguration(\n                    SDK_AUTHENTICATION_FAILURE_LISTENER_GAME_OBJECT_NAME_KEY, gameObject\n                )\n                putStringIntoRuntimeConfiguration(\n                    SDK_AUTHENTICATION_FAILURE_LISTENER_CALLBACK_METHOD_NAME_KEY, methodName\n                )\n            }\n            UnityMessageType.FEATURE_FLAGS_UPDATED -> {\n                putStringIntoRuntimeConfiguration(\n                    FEATURE_FLAGS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY, gameObject\n                )\n                putStringIntoRuntimeConfiguration(\n                    FEATURE_FLAGS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY, methodName\n                )\n            }\n        }\n    }\n\n    private fun putStringIntoRuntimeConfiguration(key: String, value: String) {\n        runtimeAppConfigurationProvider.startEdit()\n        runtimeAppConfigurationProvider.putString(key, value)\n        runtimeAppConfigurationProvider.applyEdit()\n    }\n\n    companion object {\n        private const val INAPP_SHOW_INAPP_MESSAGES_AUTOMATICALLY_KEY =\n            \"com_braze_inapp_show_inapp_messages_automatically\"\n\n        // In App Messages\n        private const val INAPP_LISTENER_GAME_OBJECT_NAME_KEY =\n            \"com_braze_inapp_listener_game_object_name\"\n        private const val INAPP_LISTENER_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_inapp_listener_callback_method_name\"\n        private const val INAPP_AUTO_SET_MANAGER_LISTENER_KEY =\n            \"com_braze_inapp_auto_set_manager_listener_key\"\n        private const val INAPP_INITIAL_DISPLAY_OPERATION_KEY =\n            \"com_braze_inapp_initial_display_operation_key\"\n\n        // News Feed listener\n        private const val FEED_LISTENER_GAME_OBJECT_NAME_KEY =\n            \"com_braze_feed_listener_game_object_name\"\n        private const val FEED_LISTENER_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_feed_listener_callback_method_name\"\n\n        // Push received\n        private const val PUSH_RECEIVED_GAME_OBJECT_NAME_KEY =\n            \"com_braze_push_received_game_object_name\"\n        private const val PUSH_RECEIVED_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_push_received_callback_method_name\"\n\n        // Push opened\n        private const val PUSH_OPENED_GAME_OBJECT_NAME_KEY =\n            \"com_braze_push_opened_game_object_name\"\n        private const val PUSH_OPENED_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_push_opened_callback_method_name\"\n\n        // Push deleted\n        private const val PUSH_DELETED_GAME_OBJECT_NAME_KEY =\n            \"com_braze_push_deleted_game_object_name\"\n        private const val PUSH_DELETED_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_push_deleted_callback_method_name\"\n\n        // Content Cards\n        private const val CONTENT_CARDS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY =\n            \"com_braze_content_cards_updated_listener_game_object_name\"\n        private const val CONTENT_CARDS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_content_cards_updated_listener_callback_method_name\"\n\n        // Feature Flags\n        private const val FEATURE_FLAGS_UPDATED_LISTENER_GAME_OBJECT_NAME_KEY =\n            \"com_braze_feature_flags_updated_listener_game_object_name\"\n        private const val FEATURE_FLAGS_UPDATED_LISTENER_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_feature_flags_updated_listener_callback_method_name\"\n\n        // SDK Authentication Failure\n        private const val SDK_AUTHENTICATION_FAILURE_LISTENER_GAME_OBJECT_NAME_KEY =\n            \"com_braze_sdk_authentication_failure_listener_game_object_name\"\n        private const val SDK_AUTHENTICATION_FAILURE_LISTENER_CALLBACK_METHOD_NAME_KEY =\n            \"com_braze_sdk_authentication_failure_listener_callback_method_name\"\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/enums/UnityInAppMessageManagerAction.kt",
    "content": "package com.braze.unity.enums\n\nimport com.braze.ui.inappmessage.InAppMessageOperation\n\nenum class UnityInAppMessageManagerAction(\n    private val value: Int,\n    val inAppMessageOperation: InAppMessageOperation?\n) {\n    UNKNOWN(-1, null),\n\n    /**\n     * Maps to [InAppMessageOperation.DISPLAY_NOW].\n     */\n    IAM_DISPLAY_NOW(0, InAppMessageOperation.DISPLAY_NOW),\n\n    /**\n     * Maps to [InAppMessageOperation.DISPLAY_LATER].\n     */\n    IAM_DISPLAY_LATER(1, InAppMessageOperation.DISPLAY_LATER),\n\n    /**\n     * Maps to [InAppMessageOperation.DISCARD].\n     */\n    IAM_DISCARD(2, InAppMessageOperation.DISCARD);\n\n    companion object {\n        fun getTypeFromValue(value: Int): UnityInAppMessageManagerAction? =\n            values().firstOrNull { it.value == value }\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/enums/UnityMessageType.kt",
    "content": "package com.braze.unity.enums\n\n/**\n * Types of messages that Braze can be configured to send to a GameObject method at runtime.\n */\n@Suppress(\"MagicNumber\")\nenum class UnityMessageType(private val value: Int) {\n    PUSH_PERMISSIONS_PROMPT_RESPONSE(0),\n    PUSH_TOKEN_RECEIVED_FROM_SYSTEM(1),\n    PUSH_RECEIVED(2),\n    PUSH_OPENED(3),\n    PUSH_DELETED(4),\n    IN_APP_MESSAGE(5),\n    NEWS_FEED(6),\n    CONTENT_CARDS_UPDATED(7),\n    SDK_AUTHENTICATION_FAILURE(8),\n    FEATURE_FLAGS_UPDATED(9);\n\n    companion object {\n        fun getTypeFromValue(value: Int) =\n            values().firstOrNull { it.value == value }\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/prime31compatible/BrazeUnityPlayerActivity.kt",
    "content": "package com.braze.unity.prime31compatible\n\nimport android.content.Intent\nimport android.os.Bundle\nimport com.braze.unity.BrazeUnityActivityWrapper\nimport com.prime31.UnityPlayerActivity\n\n/**\n * Classes in the com.braze.unity.prime31compatible package provide support for Prime31 plugins. If you\n * are using any Prime31 plugins, you must use the classes in this package INSTEAD of the classes used\n * in the com.braze.unity package.\n *\n * This is a wrapper subclass of the [UnityPlayerActivity] class. It calls the necessary Braze methods\n * to ensure that analytics are collected and that push notifications are properly forwarded to\n * the Unity application.\n */\nopen class BrazeUnityPlayerActivity : UnityPlayerActivity() {\n    private lateinit var brazeUnityActivityWrapper: BrazeUnityActivityWrapper\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        brazeUnityActivityWrapper = BrazeUnityActivityWrapper()\n        brazeUnityActivityWrapper.onCreateCalled(this)\n    }\n\n    override fun onStart() {\n        super.onStart()\n        brazeUnityActivityWrapper.onStartCalled(this)\n    }\n\n    override fun onResume() {\n        super.onResume()\n        brazeUnityActivityWrapper.onResumeCalled(this)\n    }\n\n    override fun onPause() {\n        brazeUnityActivityWrapper.onPauseCalled(this)\n        super.onPause()\n    }\n\n    override fun onStop() {\n        brazeUnityActivityWrapper.onStopCalled(this)\n        super.onStop()\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        brazeUnityActivityWrapper.onNewIntentCalled(intent, this)\n    }\n\n    fun onNewUnityInAppMessageManagerAction(actionEnumValue: Int) {\n        brazeUnityActivityWrapper.onNewUnityInAppMessageManagerAction(actionEnumValue)\n    }\n\n    fun launchContentCardsActivity() {\n        brazeUnityActivityWrapper.launchContentCardsActivity(this)\n    }\n\n    fun setInAppMessageListener() {\n        brazeUnityActivityWrapper.setInAppMessageListener()\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/utils/InAppMessageUtils.kt",
    "content": "package com.braze.unity.utils\n\nimport android.content.Context\nimport com.braze.Braze.Companion.getInstance\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.support.BrazeLogger.Priority.W\nimport com.braze.support.BrazeLogger.brazelog\n\nobject InAppMessageUtils {\n    fun inAppMessageFromString(context: Context?, messageJSONString: String?): IInAppMessage? {\n        return if (messageJSONString == null || context == null) {\n            null\n        } else {\n            getInstance(context)\n                .deserializeInAppMessageString(messageJSONString)\n        }\n    }\n\n    fun logInAppMessageClick(inAppMessage: IInAppMessage?) {\n        if (inAppMessage != null) {\n            inAppMessage.logClick()\n        } else {\n            brazelog(W) {\n                \"The in-app message is null. Not logging in-app message click.\"\n            }\n        }\n    }\n\n    fun logInAppMessageButtonClick(inAppMessage: IInAppMessage?, buttonId: Int) {\n        if (inAppMessage == null) {\n            brazelog(W) { \"The in-app message is null. Not logging in-app message button $buttonId click.\" }\n            return\n        }\n        if (inAppMessage is IInAppMessageImmersive) {\n            inAppMessage.messageButtons\n                .firstOrNull() { it.id == buttonId }\n                ?.let { inAppMessage.logButtonClick(it) }\n        } else {\n            brazelog(W) {\n                \"The in-app message isn't an instance of InAppMessageImmersive. \" +\n                    \"Not logging in-app message button click.\"\n            }\n        }\n    }\n\n    fun logInAppMessageImpression(inAppMessage: IInAppMessage?) {\n        if (inAppMessage != null) {\n            inAppMessage.logImpression()\n        } else {\n            brazelog(W) {\n                \"The in-app message is null, Not logging in-app message impression.\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android-sdk-unity/src/main/java/com/braze/unity/utils/MessagingUtils.kt",
    "content": "package com.braze.unity.utils\n\nimport android.os.Bundle\nimport com.braze.events.FeedUpdatedEvent\nimport com.braze.events.BrazePushEvent\nimport com.braze.events.BrazeSdkAuthenticationErrorEvent\nimport com.braze.events.ContentCardsUpdatedEvent\nimport com.braze.events.FeatureFlagsUpdatedEvent\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.BrazeLogger.getBrazeLogTag\nimport com.braze.support.constructJsonArray\nimport com.unity3d.player.UnityPlayer\nimport org.json.JSONObject\n\nobject MessagingUtils {\n    private val TAG = getBrazeLogTag(MessagingUtils::class.java)\n    private const val BRAZE_INTERNAL_GAME_OBJECT = \"BrazeInternalComponent\"\n\n    enum class BrazeInternalComponentMethod(val methodName: String) {\n        BEFORE_IAM_DISPLAYED(\"beforeInAppMessageDisplayed\"),\n        ON_IAM_DISMISSED(\"onInAppMessageDismissed\"),\n        ON_IAM_CLICKED(\"onInAppMessageClicked\"),\n        ON_IAM_BUTTON_CLICKED(\"onInAppMessageButtonClicked\"),\n        ON_IAM_HTML_CLICKED(\"onInAppMessageHTMLClicked\");\n    }\n\n    fun sendInAppMessageReceivedMessage(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        inAppMessage: IInAppMessage\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity GameObject configured to receive\" +\n                    \" in app messages. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive in app messages in \" +\n                    \"the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        brazelog(TAG) { \"Sending a message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(unityGameObjectName, unityCallbackFunctionName, inAppMessage.forJsonPut().toString())\n        return true\n    }\n\n    fun sendPushEventToUnity(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        event: BrazePushEvent\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"No Unity game object configured to \" +\n                    \"receive ${event.eventType} messages. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive ${event.eventType} messages \" +\n                    \"in the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        brazelog(TAG) { \"Sending a ${event.eventType} message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(\n            unityGameObjectName,\n            unityCallbackFunctionName,\n            getPushBundleExtras(event.notificationPayload.notificationExtras).toString()\n        )\n        return true\n    }\n\n    fun sendFeedUpdatedEventToUnity(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        feedUpdatedEvent: FeedUpdatedEvent\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity GameObject registered in the braze.xml configuration \" +\n                    \"file to receive feed updates. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive feed updates in \" +\n                    \"the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        val json = JSONObject()\n            .put(\"mFeedCards\", feedUpdatedEvent.feedCards.constructJsonArray())\n            .put(\"mFromOfflineStorage\", feedUpdatedEvent.isFromOfflineStorage)\n        brazelog(TAG) { \"Sending a feed updated event message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(unityGameObjectName, unityCallbackFunctionName, json.toString())\n        return true\n    }\n\n    fun sendContentCardsUpdatedEventToUnity(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        contentCardsUpdatedEvent: ContentCardsUpdatedEvent\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity GameObject configured \" +\n                    \"to receive Content Cards updated event messages. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive Content \" +\n                    \"Cards updated event messages in the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        val json = JSONObject()\n            .put(\"mContentCards\", contentCardsUpdatedEvent.allCards.constructJsonArray())\n            .put(\"mFromOfflineStorage\", contentCardsUpdatedEvent.isFromOfflineStorage)\n        brazelog(TAG) { \"Sending a Content Cards update message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(unityGameObjectName, unityCallbackFunctionName, json.toString())\n        return true\n    }\n\n    fun sendFeatureFlagsUpdatedEventToUnity(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        featureFlagsUpdatedEvent: FeatureFlagsUpdatedEvent\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity GameObject configured \" +\n                    \"to receive Feature Flags updated event messages. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive Feature \" +\n                    \"Flags updated event messages in the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        val json = JSONObject()\n            .put(\"featureFlags\", featureFlagsUpdatedEvent.featureFlags.constructJsonArray())\n        brazelog(TAG) { \"Sending Feature Flags update message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(unityGameObjectName, unityCallbackFunctionName, json.toString())\n        return true\n    }\n\n    fun sendSdkAuthErrorEventToUnity(\n        unityGameObjectName: String?,\n        unityCallbackFunctionName: String?,\n        sdkAuthError: BrazeSdkAuthenticationErrorEvent\n    ): Boolean {\n        if (unityGameObjectName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity GameObject configured \" +\n                    \"to receive SDK Authentication Failure event messages. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        if (unityCallbackFunctionName.isNullOrBlank()) {\n            brazelog(TAG) {\n                \"There is no Unity callback method name registered to receive \" +\n                    \"SDK Authentication Failure event messages in the braze.xml configuration file. Not sending the message to Unity.\"\n            }\n            return false\n        }\n        val json = JSONObject()\n            .put(\"code\", sdkAuthError.errorCode)\n            .put(\"reason\", sdkAuthError.errorReason)\n            .put(\"userId\", sdkAuthError.userId)\n            .put(\"signature\", sdkAuthError.signature)\n\n        brazelog(TAG) { \"Sending an SDK Authentication Failure message to $unityGameObjectName:$unityCallbackFunctionName.\" }\n        UnityPlayer.UnitySendMessage(unityGameObjectName, unityCallbackFunctionName, json.toString())\n        return true\n    }\n\n    /**\n     * Sends some structured data to the BrazeInternalComponent in C# in the Unity binding.\n     */\n    fun sendToBrazeInternalComponent(method: BrazeInternalComponentMethod, json: String) {\n        UnityPlayer.UnitySendMessage(BRAZE_INTERNAL_GAME_OBJECT, method.methodName, json)\n    }\n\n    /**\n     * De-serializes a bundle into a key value pair that can be represented as a [JSONObject].\n     * Nested bundles are also converted recursively to have a single hierarchical structure.\n     *\n     * @param pushExtras The bundle received whenever a push notification is received, opened or deleted.\n     * @return A [JSONObject] that represents this bundle in string format.\n     */\n    internal fun getPushBundleExtras(pushExtras: Bundle?): JSONObject {\n        val json = JSONObject()\n        if (pushExtras == null) {\n            return json\n        }\n        for (key in pushExtras.keySet()) {\n            @Suppress(\"DEPRECATION\")\n            pushExtras[key]?.let {\n                if (it is Bundle) {\n                    json.put(key, getPushBundleExtras(it).toString())\n                } else {\n                    json.put(key, it.toString())\n                }\n            }\n        }\n        return json\n    }\n}\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n  repositories {\n    mavenLocal()\n    google()\n    mavenCentral()\n    maven {\n      name = 'ajoberstar-backup'\n      url = 'https://ajoberstar.org/bintray-backup/'\n    }\n  }\n\n  dependencies {\n    classpath 'com.android.tools.build:gradle:7.1.0'\n    classpath 'com.google.gms:google-services:4.3.4'\n    classpath \"org.ajoberstar.grgit:grgit-gradle:${AJOBERSTAR_GIT_GRADLE_PLUGIN_VERSION}\"\n    classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:${KOTLIN_VERSION}\"\n  }\n}\n\next {\n  compileSdkVersion = 33\n  buildToolsVersion = '33.0.0'\n  minSdkVersion = 16\n  targetSdkVersion = 33\n  appVersionName = '24.3.0'\n}\n\nsubprojects {\n  repositories {\n    maven { url \"https://appboy.github.io/appboy-android-sdk/sdk\" }\n    mavenLocal()\n    google()\n    mavenCentral()\n  }\n\n  group = 'com.appboy'\n  version = '24.3.0'\n}\n"
  },
  {
    "path": "config/buildscript/break-compile-on-deprecations.gradle",
    "content": "apply plugin: 'kotlin-android'\n\ntasks.withType(JavaCompile) {\n  options.deprecation = true\n  options.compilerArgs += ['-Xlint:deprecation']\n\n  // Only print warnings for tests, don't break the build\n  if (!it.name.toLowerCase().contains(\"debug\")) {\n    options.compilerArgs += ['-Werror']\n  }\n}\n\nproject.tasks.whenTaskAdded { task ->\n  var tn = task.name.toLowerCase()\n  if (tn.contains(\"kotlin\")) {\n    kotlinOptions {\n      allWarningsAsErrors = !tn.contains(\"debug\")\n    }\n  }\n}\n"
  },
  {
    "path": "config/quality/break-compile-on-deprecations.gradle",
    "content": "tasks.withType(JavaCompile) {\n  options.deprecation = true\n  options.compilerArgs += ['-Xlint:deprecation']\n\n  // Only print warnings for tests, don't break the build\n  if (!it.name.toLowerCase().contains(\"debug\")) {\n    options.compilerArgs += ['-Werror']\n  }\n}\n"
  },
  {
    "path": "droidboy/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\ndependencies {\n  implementation project(':android-sdk-ui')\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n  implementation \"androidx.preference:preference:${ANDROIDX_PREFERENCE_VERSION}\"\n  implementation \"com.google.android.material:material:${GOOGLE_MATERIAL_VERSION}\"\n  implementation \"androidx.swiperefreshlayout:swiperefreshlayout:${ANDROIDX_SWIPE_REFRESH_LAYOUT_VERSION}\"\n  implementation \"androidx.constraintlayout:constraintlayout:${ANDROIDX_CONSTRAINT_LAYOUT_VERSION}\"\n\n  implementation \"com.google.android.gms:play-services-location:${PLAY_SERVICES_LOCATION_VERSION}\"\n  implementation \"com.google.android.gms:play-services-maps:${PLAY_SERVICES_MAPS_VERSION}\"\n\n  implementation \"com.github.bumptech.glide:glide:${GLIDE_VERSION}\"\n\n  implementation \"com.google.firebase:firebase-core:${FIREBASE_CORE_VERSION}\"\n  implementation \"com.google.firebase:firebase-messaging:${FIREBASE_PUSH_MESSAGING_VERSION}\"\n  implementation \"com.google.android.gms:play-services-mlkit-barcode-scanning:${GOOGLE_ML_VISION_BARCODE}\"\n  implementation \"com.google.firebase:firebase-crashlytics:${FIREBASE_CRASHLYTICS_VERSION}\"\n\n  implementation \"org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}\"\n  implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:${KOTLIN_COROUTINES_VERSION}\")\n  implementation \"androidx.lifecycle:lifecycle-runtime-ktx:${KOTLIN_LIFECYCLE_RUNTIME_KTX_VERSION}\"\n  implementation \"androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}\"\n}\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    applicationId \"com.appboy.sample\"\n    versionName rootProject.ext.appVersionName\n    versionCode 1\n    resValue \"string\", \"google_maps_key\", (project.findProperty(\"GOOGLE_MAPS_API_KEY\") ?: \"\")\n\n    buildConfigField \"String\", \"BUILD_TIME\", \"\\\"\" + new Date() + \"\\\"\"\n    buildConfigField \"boolean\", \"IS_DROIDBOY_RELEASE_BUILD\", \"false\"\n    buildConfigField \"String\", \"SDK_AUTH_ENDPOINT\", \"\\\"\\\"\"\n    buildConfigField \"boolean\", \"STRICTMODE_ENABLED\", \"false\"\n\n    multiDexEnabled true\n  }\n\n  lintOptions {\n    disable 'MissingTranslation'\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n\n  kotlinOptions {\n    freeCompilerArgs = ['-Xjvm-default=all']\n    jvmTarget = \"1.8\"\n  }\n}\n"
  },
  {
    "path": "droidboy/gradle.properties",
    "content": "GOOGLE_MAPS_API_KEY=AIzaSyDcttBnAHYZuR2fhWj-f74LMj3SG5MdAuQ\n"
  },
  {
    "path": "droidboy/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:amazon=\"http://schemas.amazon.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  package=\"com.appboy.sample\">\n\n  <application\n    android:name=\".DroidboyApplication\"\n    android:allowBackup=\"false\"\n    android:hardwareAccelerated=\"true\"\n    android:icon=\"@mipmap/ic_launcher_droidboy\"\n    android:label=\"@string/app_name\"\n    android:theme=\"@style/Theme.Droidboy\"\n    android:usesCleartextTraffic=\"true\">\n    <activity\n      android:name=\".activity.InAppMessageSandboxActivity\"\n      android:exported=\"true\" />\n    <activity\n      android:name=\".activity.GeofencesMapActivity\"\n      android:exported=\"true\" />\n    <activity\n      android:name=\".activity.DroidBoyActivity\"\n      android:exported=\"true\"\n      android:icon=\"@mipmap/ic_launcher_droidboy\"\n      android:windowSoftInputMode=\"stateUnchanged\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n    </activity>\n    <activity\n      android:name=\".activity.SettingsActivity\"\n      android:exported=\"true\"\n      android:label=\"Preferences\"\n      android:theme=\"@style/Theme.Droidboy\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.VIEW\" />\n\n        <category android:name=\"android.intent.category.DEFAULT\" />\n        <category android:name=\"android.intent.category.BROWSABLE\" />\n        <!-- Accepts URI \"droidboy://preferences” -->\n        <data\n          android:host=\"preferences\"\n          android:scheme=\"droidboy\" />\n      </intent-filter>\n      <intent-filter>\n        <action android:name=\"android.intent.action.VIEW\" />\n\n        <category android:name=\"android.intent.category.DEFAULT\" />\n        <category android:name=\"android.intent.category.BROWSABLE\" />\n        <!-- Accepts URIs that begin with \"https://www.droidboy.com/preferences” -->\n        <data\n          android:host=\"www.droidboy.com\"\n          android:pathPrefix=\"/preferences\"\n          android:scheme=\"https\" />\n      </intent-filter>\n    </activity>\n    <activity android:name=\".activity.FeedFragmentActivity\" />\n\n    <meta-data\n      android:name=\"io.branch.sdk.BranchKey\"\n      android:value=\"key_live_flzo8WNh8i9tdQl713oTFkhlrri9uA8k\" />\n    <meta-data\n      android:name=\"com.google.android.gms.version\"\n      android:value=\"@integer/google_play_services_version\" />\n    <meta-data\n      android:name=\"com.google.android.geo.API_KEY\"\n      android:value=\"@string/google_maps_key\" />\n    <meta-data\n      android:name=\"com.google.firebase.ml.vision.DEPENDENCIES\"\n      android:value=\"barcode\" />\n\n    <provider\n      android:name=\"androidx.core.content.FileProvider\"\n      android:authorities=\"${applicationId}.fileprovider\"\n      android:exported=\"false\"\n      android:grantUriPermissions=\"true\">\n      <meta-data\n        android:name=\"android.support.FILE_PROVIDER_PATHS\"\n        android:resource=\"@xml/provider_filepaths\" />\n    </provider>\n\n    <receiver\n      android:name=\"com.braze.push.BrazeAmazonDeviceMessagingReceiver\"\n      android:exported=\"true\"\n      android:permission=\"com.amazon.device.messaging.permission.SEND\">\n      <intent-filter>\n        <action android:name=\"com.amazon.device.messaging.intent.RECEIVE\" />\n        <action android:name=\"com.amazon.device.messaging.intent.REGISTRATION\" />\n\n        <category android:name=\"${applicationId}\" />\n      </intent-filter>\n    </receiver>\n    <receiver\n      android:name=\"com.braze.BrazeBootReceiver\"\n      android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n      </intent-filter>\n    </receiver>\n    <service\n      android:name=\"com.braze.push.BrazeFirebaseMessagingService\"\n      android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n      </intent-filter>\n    </service> <!-- For the environment barcode reader -->\n    <uses-library\n      android:name=\"org.apache.http.legacy\"\n      android:required=\"false\" />\n    <!-- File provider for logcat exporting -->\n    <!-- https://developer.android.com/training/secure-file-sharing/setup-sharing -->\n    <amazon:enable-feature\n      android:name=\"com.amazon.device.messaging\"\n      android:required=\"false\" />\n  </application>\n  <permission\n    android:name=\"${applicationId}.permission.RECEIVE_ADM_MESSAGE\"\n    android:protectionLevel=\"signature\" />\n\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n  <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n  <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n  <uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" /> <!-- Permissions for ADM -->\n  <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n  <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n  <uses-permission android:name=\"${applicationId}.permission.RECEIVE_ADM_MESSAGE\" />\n  <uses-permission android:name=\"com.amazon.device.messaging.permission.RECEIVE\" />\n  <uses-sdk tools:overrideLibrary=\"com.google.firebase.messaging\"/>\n</manifest>\n"
  },
  {
    "path": "droidboy/src/main/assets/braze_actions/show_push_prompt.txt",
    "content": "brazeActions://v1/ewAiAHQAeQBwAGUAIgA6ACIAcgBlAHEAdQBlAHMAdABQAHUAcwBoAFAAZQByAG0AaQBzAHMAaQBvAG4AIgB9AA\n"
  },
  {
    "path": "droidboy/src/main/assets/html_in_app_message_bridge_tester.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head lang=\"en\">\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\" />\n    <script type=\"text/javascript\" src=\"HTML_ZIP_STOPWATCH/myscript.js\"></script>\n    <script type=\"text/javascript\">\n      function setCustomAttributes() {\n        brazeBridge.getUser().setCustomUserAttribute('abStringAttribute', 'atts');\n        brazeBridge.getUser().setCustomUserAttribute('abDateAttribute', new Date());\n        brazeBridge.getUser().setCustomUserAttribute('abIntAttribute', 6);\n        brazeBridge.getUser().setCustomUserAttribute('abDoubleAttribute', 7.8);\n        brazeBridge.getUser().setCustomUserAttribute('abBoolAttribute', true);\n        brazeBridge.getUser().setCustomUserAttribute('abArrayAttribute', ['a', 'b']);\n        brazeBridge.getUser().setLocationCustomUserAttribute('Dutch Restaurant', 52.369838, 4.875981);\n      };\n      function logCustomEvents() {\n        brazeBridge.logCustomEvent('abEvent');\n        brazeBridge.logCustomEvent('abEventNullProp', null);\n        brazeBridge.logCustomEvent('abEventWithProp', { intProp: 1, doubleProp: 4.4, stringProp: 'asdf', boolProp: true, dateProp:new Date()});\n      };\n      function logPurchases() {\n        brazeBridge.logPurchase('abPurchase', 5.5, 'USD');\n        brazeBridge.logPurchase('abPurchaseWithQuantity', 5.5, 'USD', 6);\n        brazeBridge.logPurchase('abPurchaseWithQuantityAndProperties', 5.5, 'USD', 6, { intProp: 1, doubleProp: 4.4, stringProp: 'string property', boolProp: false, dateProp:new Date()});\n        brazeBridge.logPurchase('abPurchaseWithPropertiesNoQuantity', 5.5, 'USD', null, { intProp: 1, doubleProp: 4.4, stringProp: 'string property', boolProp: false, dateProp:new Date()});\n      };\n    </script>\n    <link href=\"HTML_ZIP_STOPWATCH/iamcss.css\" rel=\"stylesheet\" type=\"text/css\" />\n    <style>\n    a, button {\n      color: blue;\n      padding: 2px 25px;\n      font-family: \"Avenir\";\n      text-transform: none;\n    }\n    button {\n      display: block;\n      margin: 4px auto;\n      background: antiquewhite;\n      width: 75%;\n      padding: 10px;\n    }\n    .firstLink {\n      clear: right;\n    }\n    </style>\n    <script type=\"text/javascript\">\n      function logStartEvent() {\n        brazeBridge.logCustomEvent('Logging start event for HTML in-app message tester');\n      }\n      window.addEventListener('ab.BridgeReady', logStartEvent, false);\n    </script>\n</head>\n\n<body>\n<div class=\"box\">\n    <div class=\"relativeTopRight\">\n        <a href=\"appboy://close\">X</a>\n    </div>\n\n    <a class=\"firstLink\" href=\"https://www.appboy.com?abExternalOpen=false&abButtonId=1\">Appboy.com</a>\n    <button onClick=\"document.body.style.backgroundColor = 'rgba(232, 125, 232, .75)'\">Make more transparent</button>\n    <a href=\"droidboy://preferences\">Deep Link</a>\n    <a href=\"appboy://feed\">Open the Newsfeed</a>\n    <button id=\"color-change\" onclick=\"javascript:sayHi();\">Change Color To Red</button>\n    <button onclick=\"window.location = 'appboy://customEvent?name=androidHtmlIamCustomEvent&pOne=pTwo'\">Custom Event Url Style</button>\n    <button onClick=\"brazeBridge.closeMessage()\" />ab: closeMessage</button>\n    <button onClick=\"brazeBridge.requestImmediateDataFlush()\" />ab: requestImmediateDataFlush</button>\n    <button onClick=\"logCustomEvents()\" />ab: logCustomEvent</button>\n    <button onClick=\"logPurchases()\" />ab: logPurchase</button>\n    <button onClick=\"brazeBridge.display.showFeed()\" />ab: display.showFeed</button>\n    <button onClick=\"brazeBridge.getUser().setFirstName('abFirstName')\" />ab: setFirstName</button>\n    <button onClick=\"brazeBridge.getUser().setLastName('abLastName')\" />ab: setLastName</button>\n    <button onClick=\"brazeBridge.getUser().setEmail('abEmail@appboy.com')\" />ab: setEmail</button>\n    <button onClick=\"brazeBridge.getUser().setHomeCity('abHomeCity')\" />ab: setHomeCity</button>\n    <button onClick=\"brazeBridge.getUser().setEmailNotificationSubscriptionType('unsubscribed')\" />ab: setEmailNotificationSubscriptionType Unsubscribed</button>\n    <button onClick=\"brazeBridge.getUser().setEmailNotificationSubscriptionType('subscribed')\" />ab: setEmailNotificationSubscriptionType Subscribed</button>\n    <button onClick=\"brazeBridge.getUser().setEmailNotificationSubscriptionType('opted_in')\" />ab: setEmailNotificationSubscriptionType Opted-in</button>\n    <button onClick=\"brazeBridge.getUser().setPushNotificationSubscriptionType('unsubscribed')\" />ab: setPushNotificationSubscriptionType</button>\n    <button onClick=\"brazeBridge.getUser().addToCustomAttributeArray('abAtArray', 'myval')\" />ab: addToCustomAttributeArray</button>\n    <button onClick=\"brazeBridge.getUser().removeFromCustomAttributeArray('abAtArray', 'myval')\" />ab: removeFromCustomAttributeArray</button>\n    <button onClick=\"brazeBridge.getUser().incrementCustomUserAttribute('abInc')\" />ab: incrementCustomUserAttribute</button>\n    <button onClick=\"brazeBridge.getUser().setDateOfBirth(1960, 11, 11)\" />ab: setDateOfBirth</button>\n    <button onClick=\"brazeBridge.getUser().setCountry('USA')\" />ab: setCountry</button>\n    <button onClick=\"brazeBridge.getUser().setPhoneNumber('9084894848')\" />ab: setPhone</button>\n    <button onClick=\"setCustomAttributes()\" />ab: setCustomAttributes</button>\n    <button onClick=\"brazeBridge.getUser().setGender('m')\" />ab: setGender Male</button>\n    <button onClick=\"brazeBridge.getUser().setGender('f')\" />ab: setGender Female</button>\n    <button onClick=\"brazeBridge.getUser().setGender('o')\" />ab: setGender Other</button>\n    <button onClick=\"brazeBridge.getUser().setGender('u')\" />ab: setGender Unknown</button>\n    <button onClick=\"brazeBridge.getUser().setGender('n')\" />ab: setGender Not Applicable</button>\n    <button onClick=\"brazeBridge.getUser().setGender('p')\" />ab: setGender Prefer not to Say</button>\n    <button onClick=\"brazeBridge.getUser().setLocationCustomUserAttribute('Foodhallen', 52.367070, 4.868143)\" />ab: setLocationCustomUserAttribute</button>\n    <button onClick=\"brazeBridge.getUser().addToSubscriptionGroup('123456')\" />ab: addToSubscriptionGroup</button>\n    <button onClick=\"brazeBridge.getUser().removeFromSubscriptionGroup('654321')\" />ab: removeFromSubscriptionGroup</button>\n    <button onClick=\"brazeBridge.logClick('1')\" />ab: logClick 1</button>\n    <button onClick=\"brazeBridge.logClick()\" />ab: logClick no arg</button>\n    <button onClick=\"brazeBridge.requestPushPermission()\" />ab: requestPushPermission</button>\n    <button onClick=\"brazeBridge.web.registerAppboyPushMessages()\" />ab: web.registerAppboyPushMessages()</button>\n    <button onClick=\"brazeBridge.web.trackLocation()\" />ab: web.trackLocation()</button>\n\n    <img src=\"HTML_ZIP_STOPWATCH/jpg_3.jpg\" width=\"100%\">\n    <div class=\"relativeLeft\">\n        <a href=\"https://twitter.com/appboy?abButtonId=0&abExternalOpen=true\">Appboy Twitter</a>\n    </div>\n    <div class=\"relativeRight\">\n        <a href=\"appboy://close\">Close</a>\n    </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_in_app_message_unified_bootstrap_album.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <link rel=\"icon\" href=\"/docs/4.0/assets/img/favicons/favicon.ico\">\n\n    <title>Album example for Bootstrap</title>\n\n    <link rel=\"canonical\" href=\"https://getbootstrap.com/docs/4.0/examples/album/\">\n\n    <!-- Bootstrap core CSS -->\n    <link href=\"https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">\n\n    <!-- Custom styles for this template -->\n    <link href=\"https://getbootstrap.com/docs/4.0/examples/album/album.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n\n<header>\n    <div class=\"collapse bg-dark\" id=\"navbarHeader\">\n        <div class=\"container\">\n            <div class=\"row\">\n                <div class=\"col-sm-8 col-md-7 py-4\">\n                    <h4 class=\"text-white\">About</h4>\n                    <p class=\"text-muted\">Add some information about the album below, the author, or any other background\n                        context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off\n                        to some social networking sites or contact information.</p>\n                </div>\n                <div class=\"col-sm-4 offset-md-1 py-4\">\n                    <h4 class=\"text-white\">Contact</h4>\n                    <ul class=\"list-unstyled\">\n                        <li><a href=\"https://www.braze.com/?abExternalOpen=true&abButtonId=1\" class=\"text-white\">Braze.com in chrome</a></li>\n                        <li><a href=\"https://www.braze.com/?abExternalOpen=false&abButtonId=2\" class=\"text-white\">Braze.com in in-app browser</a></li>\n                        <li><a href=\"mailto:droidboy@braze.com\" class=\"text-white\">Email me</a></li>\n                    </ul>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"navbar navbar-dark bg-dark box-shadow\">\n        <div class=\"container d-flex justify-content-between\">\n            <a href=\"#\" class=\"navbar-brand d-flex align-items-center\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\"\n                     stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"mr-2\">\n                    <path d=\"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z\"></path>\n                    <circle cx=\"12\" cy=\"13\" r=\"4\"></circle>\n                </svg>\n                <strong>Album</strong>\n            </a>\n            <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarHeader\"\n                    aria-controls=\"navbarHeader\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n                <span class=\"navbar-toggler-icon\"></span>\n            </button>\n        </div>\n    </div>\n</header>\n\n<main role=\"main\">\n\n    <section class=\"jumbotron text-center\">\n        <div class=\"container\">\n            <h1 class=\"jumbotron-heading\">Album example</h1>\n            <p class=\"lead text-muted\">Something short and leading about the collection below—its contents, the creator,\n                etc. Make it short and sweet, but not too short so folks don't simply skip over it entirely.</p>\n            <p>\n                <a href=\"#\" class=\"btn btn-primary my-2\">Main call to action</a>\n                <a href=\"#\" class=\"btn btn-secondary my-2\">Secondary action</a>\n            </p>\n        </div>\n    </section>\n\n    <div class=\"album py-5 bg-light\">\n        <div class=\"container\">\n\n            <div class=\"row\">\n                <div class=\"col-md-4\">\n                    <div class=\"card mb-4 box-shadow\">\n                        <img src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de53a531a3ff3e7ef46/original.jpeg?1623731685\" class=\"card-img-top\" alt=\"Card image cap\">\n                        <div class=\"card-body\">\n                            <p class=\"card-text\">This is a wider card with supporting text below as a natural lead-in to additional\n                                content. This content is a little bit longer.</p>\n                            <div class=\"d-flex justify-content-between align-items-center\">\n                                <div class=\"btn-group\">\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" onclick=\"javascript:brazeBridge.logCustomEvent('button spark clicked');\">Log Custom Event</button>\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\">Edit</button>\n                                </div>\n                                <small class=\"text-muted\">9 mins</small>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"col-md-4\">\n                    <div class=\"card mb-4 box-shadow\">\n                        <img src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de567360e2ab3ac9cf1/original.jpeg?1623731685\" class=\"card-img-top\" alt=\"Card image cap\">\n                        <div class=\"card-body\">\n                            <p class=\"card-text\">This is a wider card with supporting text below as a natural lead-in to additional\n                                content. This content is a little bit longer.</p>\n                            <div class=\"d-flex justify-content-between align-items-center\">\n                                <div class=\"btn-group\">\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\">View</button>\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\">Edit</button>\n                                </div>\n                                <small class=\"text-muted\">9 mins</small>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"col-md-4\">\n                    <div class=\"card mb-4 box-shadow\">\n                        <img src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de467360e7d35ac9e5f/original.jpeg?1623731684\" class=\"card-img-top\" alt=\"Card image cap\">\n                        <div class=\"card-body\">\n                            <p class=\"card-text\">This is a wider card with supporting text below as a natural lead-in to additional\n                                content. This content is a little bit longer.</p>\n                            <div class=\"d-flex justify-content-between align-items-center\">\n                                <div class=\"btn-group\">\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\">View</button>\n                                    <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\">Edit</button>\n                                </div>\n                                <small class=\"text-muted\">9 mins</small>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    </div>\n\n</main>\n\n<footer class=\"text-muted\">\n    <div class=\"container\">\n        <p class=\"float-right\">\n            <a href=\"#\">Back to top</a>\n        </p>\n        <p>Album example is &copy; Bootstrap, but please download and customize it for yourself!</p>\n    </div>\n</footer>\n\n<!-- Bootstrap core JavaScript\n  ================================================== -->\n<!-- Placed at the end of the document so the pages load faster -->\n<script src=\"https://code.jquery.com/jquery-3.2.1.slim.min.js\"\n        integrity=\"sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN\"\n        crossorigin=\"anonymous\"></script>\n<script>window.jQuery || document.write('<script src=\"https://getbootstrap.com/assets/js/vendor/jquery-slim.min.js\"><\\/script>')</script>\n<script src=\"https://getbootstrap.com/docs/4.0/assets/js/vendor/popper.min.js\"></script>\n<script src=\"https://getbootstrap.com/docs/4.0/dist/js/bootstrap.min.js\"></script>\n<script src=\"https://getbootstrap.com/docs/4.0/assets/js/vendor/holder.min.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_body_external_js.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\"/>\n    <script type=\"text/javascript\" src=\"myscript.js\"></script>\n  </head>\n  <body  bgcolor=\"red\">\n    <button id=\"color-change\" onclick='javascript:sayHi();'>Change Color To Red</button>\n  </body>\n</html>"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_body_inline_js.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\"/>\n  </head>\n  <body style=\"background-color: rgba(0, 115, 213, .90)\">\n    <button id=\"color-change\" onclick=\"document.getElementById('color-change').style.backgroundColor='red'; console.log('Just a debug log message');\">Change Color To Red</button>\n    <a onclick=\"window.location='appboy://customEvent?name=htmlIAMCustomEvent&p1=p2'\">Custom Event</a>\n  </body>\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_body_no_js.html",
    "content": "<!DOCTYPE html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n    <style>\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Medium.ttf) format(\"ttf\");\n        font-weight: 400;\n    }\n\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Regular.ttf) format(\"ttf\");\n        font-weight: 300;\n    }\n\n    * {\n        -webkit-box-sizing: border-box;\n        -moz-box-sizing: border-box;\n        box-sizing: border-box;\n    }\n\n    body {\n        background: transparent;\n        font-family: \"Roboto\", sans-serif;\n        -ms-text-size-adjust: 100%;\n        -webkit-text-size-adjust: 100%;\n        color: #333;\n        text-align: left;\n    }\n\n    .box {\n        background: #0073d5;\n        border-radius: 2px;\n        width: 100%;\n        height: 100%;\n        min-width: 220px;\n        min-height: 220px;\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n    }\n\n    a {\n        display: block;\n        text-align: center;\n        text-decoration: none;\n        text-transform: uppercase;\n        color: #fff;\n        padding: 20px;\n    }  \n\n    div.relativeLeft {\n        position: relative;\n        top: 0px;\n        right: 0;\n        width: 0px;\n        height: 0px;\n    }\n\n    div.relativeRight {\n        position: relative;\n        float: right;\n    }\n\n    div.relativeTopRight {\n        position: relative;\n        float: top;\n        float: right;\n    }\n\n    </style>\n</head>\n<body>\n    <div class=\"box\">\n        <div class=\"relativeTopRight\">\n            <a href=\"appboy://close\">X</a>\n        </div>\n        <a href=\"http://braze.com?abDeepLink=true\">Visit Appboy.com</a>\n        <a href=\"https://github.com/Appboy/appboy-android-sdk\">Visit Braze Android SDK</a>\n        <a href=\"tel://15043277269\">Call Braze</a>\n        <a href=\"mailto:droidboy@braze.com\">Email Droidboy</a>\n        <a href=\"appboy://feed\">Open the Newsfeed</a>\n        <p style=\"text-align:center;\">\n            <img src=\"device appboy.png\" alt=\"Appboy\" style=\"width:192px;height:108px;vertical-align:middle\">\n        </p>\n        <div class=\"relativeLeft\">\n            <a href=\"https://twitter.com/braze?abButtonId=0&abExternalOpen=true\">Braze Twitter</a>\n        </div>\n        <div class=\"relativeRight\">\n            <a href=\"appboy://close?abButtonId=1\">Close</a>\n        </div>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_body_star_wars.html",
    "content": "<!DOCTYPE html>\n<!-- saved from url=(0071)http://blogs.sitepointstatic.com/examples/tech/css3-starwars/index.html -->\n<html lang=\"en\"><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n  <meta charset=\"UTF-8\">\n  <title>CSS Star Wars Crawling Titles</title>\n\n  <style>\n    /* latin */\n    @font-face {\n    font-family: 'Droid Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: local('Droid Sans'), local('DroidSans'), url(http://fonts.gstatic.com/s/droidsans/v6/s-BiyweUPV0v-yRb-cjciAzyDMXhdD8sAj6OAJTFsBI.woff2) format('woff2');\n    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;\n    }\n    /* latin */\n    @font-face {\n    font-family: 'Droid Sans';\n    font-style: normal;\n    font-weight: 700;\n    src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(http://fonts.gstatic.com/s/droidsans/v6/EFpQQyG9GqCrobXxL-KRMWaVI6zN22yiurzcBKxPjFE.woff2) format('woff2');\n    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;\n    }\n\n    * { padding: 0; margin: 0; }\n\n    body, html\n    {\n    width: 100%;\n    height: 100%;\n    font-family: \"Droid Sans\", arial, verdana, sans-serif;\n    font-weight: 700;\n    color: #ff6;\n    background-color: #000;\n    overflow: hidden;\n    }\n\n    p#start\n    {\n    position: relative;\n    width: 16em;\n    font-size: 200%;\n    font-weight: 400;\n    margin: 20% auto;\n    color: #4ee;\n    opacity: 0;\n    z-index: 1;\n    -webkit-animation: intro 2s ease-out;\n    -moz-animation: intro 2s ease-out;\n    -ms-animation: intro 2s ease-out;\n    -o-animation: intro 2s ease-out;\n    animation: intro 2s ease-out;\n    }\n\n    @-webkit-keyframes intro {\n    0% { opacity: 1; }\n    90% { opacity: 1; }\n    100% { opacity: 0; }\n    }\n\n    @-moz-keyframes intro {\n    0% { opacity: 1; }\n    90% { opacity: 1; }\n    100% { opacity: 0; }\n    }\n\n    @-ms-keyframes intro {\n    0% { opacity: 1; }\n    90% { opacity: 1; }\n    100% { opacity: 0; }\n    }\n\n    @-o-keyframes intro {\n    0% { opacity: 1; }\n    90% { opacity: 1; }\n    100% { opacity: 0; }\n    }\n\n    @keyframes intro {\n    0% { opacity: 1; }\n    90% { opacity: 1; }\n    100% { opacity: 0; }\n    }\n\n    h1\n    {\n    position: absolute;\n    width: 2.6em;\n    left: 50%;\n    top: 25%;\n    font-size: 10em;\n    text-align: center;\n    margin-left: -1.3em;\n    line-height: 0.8em;\n    letter-spacing: -0.05em;\n    color: #000;\n    text-shadow: -2px -2px 0 #ff6, 2px -2px 0 #ff6, -2px 2px 0 #ff6, 2px 2px 0 #ff6;\n    opacity: 0;\n    z-index: 1;\n    -webkit-animation: logo 5s ease-out 2.5s;\n    -moz-animation: logo 5s ease-out 2.5s;\n    -ms-animation: logo 5s ease-out 2.5s;\n    -o-animation: logo 5s ease-out 2.5s;\n    animation: logo 5s ease-out 2.5s;\n    }\n\n    h1 sub\n    {\n    display: block;\n    font-size: 0.3em;\n    letter-spacing: 0;\n    line-height: 0.8em;\n    }\n\n    @-webkit-keyframes logo {\n    0% { -webkit-transform: scale(1); opacity: 1; }\n    50% { opacity: 1; }\n    100% { -webkit-transform: scale(0.1); opacity: 0; }\n    }\n\n    @-moz-keyframes logo {\n    0% { -moz-transform: scale(1); opacity: 1; }\n    50% { opacity: 1; }\n    100% { -moz-transform: scale(0.1); opacity: 0; }\n    }\n\n    @-ms-keyframes logo {\n    0% { -ms-transform: scale(1); opacity: 1; }\n    50% { opacity: 1; }\n    100% { -ms-transform: scale(0.1); opacity: 0; }\n    }\n\n    @-o-keyframes logo {\n    0% { -o-transform: scale(1); opacity: 1; }\n    50% { opacity: 1; }\n    100% { -o-transform: scale(0.1); opacity: 0; }\n    }\n\n    @keyframes logo {\n    0% { transform: scale(1); opacity: 1; }\n    50% { opacity: 1; }\n    100% { transform: scale(0.1); opacity: 0; }\n    }\n\n    /* the interesting 3D scrolling stuff */\n    #titles\n    {\n    position: absolute;\n    width: 18em;\n    height: 50em;\n    bottom: 0;\n    left: 50%;\n    margin-left: -9em;\n    font-size: 350%;\n    text-align: justify;\n    overflow: hidden;\n    -webkit-transform-origin: 50% 100%;\n    -moz-transform-origin: 50% 100%;\n    -ms-transform-origin: 50% 100%;\n    -o-transform-origin: 50% 100%;\n    transform-origin: 50% 100%;\n    -webkit-transform: perspective(300px) rotateX(25deg);\n    -moz-transform: perspective(300px) rotateX(25deg);\n    -ms-transform: perspective(300px) rotateX(25deg);\n    -o-transform: perspective(300px) rotateX(25deg);\n    transform: perspective(300px) rotateX(25deg);\n    }\n\n    #titles:after\n    {\n    position: absolute;\n    content: ' ';\n    left: 0;\n    right: 0;\n    top: 0;\n    bottom: 60%;\n    background-image: -webkit-linear-gradient(top, rgba(0,0,0,1) 0%, transparent 100%);\n    background-image: -moz-linear-gradient(top, rgba(0,0,0,1) 0%, transparent 100%);\n    background-image: -ms-linear-gradient(top, rgba(0,0,0,1) 0%, transparent 100%);\n    background-image: -o-linear-gradient(top, rgba(0,0,0,1) 0%, transparent 100%);\n    background-image: linear-gradient(top, rgba(0,0,0,1) 0%, transparent 100%);\n    pointer-events: none;\n    }\n\n    #titles p\n    {\n    text-align: justify;\n    margin: 0.8em 0;\n    }\n\n    #titles p.center\n    {\n    text-align: center;\n    }\n\n    #titles a\n    {\n    color: #ff6;\n    text-decoration: underline;\n    }\n\n    #titlecontent\n    {\n    position: absolute;\n    top: 100%;\n    -webkit-animation: scroll 100s linear 4s infinite;\n    -moz-animation: scroll 100s linear 4s infinite;\n    -ms-animation: scroll 100s linear 4s infinite;\n    -o-animation: scroll 100s linear 4s infinite;\n    animation: scroll 100s linear 4s infinite;\n    }\n\n    /* animation */\n    @-webkit-keyframes scroll {\n    0% { top: 100%; }\n    100% { top: -170%; }\n    }\n\n    @-moz-keyframes scroll {\n    0% { top: 100%; }\n    100% { top: -170%; }\n    }\n\n    @-ms-keyframes scroll {\n    0% { top: 100%; }\n    100% { top: -170%; }\n    }\n\n    @-o-keyframes scroll {\n    0% { top: 100%; }\n    100% { top: -170%; }\n    }\n\n    @keyframes scroll {\n    0% { top: 100%; }\n    100% { top: -170%; }\n    }\n  </style>\n\n  <style type=\"text/css\"></style></head>\n<body>\n\n<p id=\"start\">A short time ago in an app very, very close…</p>\n\n<h1>STAR WARS</h1>\n\n<div id=\"titles\"><div id=\"titlecontent\">\n\n  <p class=\"center\">EPISODE HTML IN-APP MESSAGES<br>A NEW TOOL FOR APPBOY</p>\n\n  <p>It is a period of engagement war.</p>\n\n  <p>This is a demonstration of Star Wars-style scrolling 3D titles in CSS3 and HTML using Braze In-App Messages</p>\n\n  <p>You can do virtually anything with HTML IN-APP Messages. Multi-Screen messages, logging custom events from messages. Anything you want.</p>\n\n  <p>Also, \"Star wars\" is amazing, and so is the flexibility of HTML!</p>\n\n  <p>So how does it work? Well, it's fairly simple. We have an outer absolute DIV (#titles) which is rotated along the X-axis using perspective to give the impression of depth. The same DIV also has an :after psuedo-element which applies a linear gradient so the text appears to fade out.</p>\n\n  <p>Inside, we have another absolutely-positioned DIV which contains the text (#titlecontent). The top is set to 100% to ensure it starts off-screen then uses CSS3 animation to move it upward over time. No JavaScript is required.</p>\n\n  <p>You will probably need to adjust the movement amount and timing depending on the quantity of text you want to show. The 3D depth can also be tweaked in the #titles declaration.</p>\n\n  <p>All the code is contained in this single HTML file…\n\n  </p><p class=\"center\">View the source, Luke!</p>\n\n  <p>Sorry. Couldn't resist it.</p>\n\n  <p>You're welcome to use this demonstration code in your own sites. Source code modified from: </p>\n\n  <p class=\"center\"><a href=\"http://www.sitepoint.com/css3-starwars-scrolling-text/\">sitepoint.com/<br>css3-starwars-scrolling-text/</a></p>\n\n  <p>Finally, Han shot first and the original, unadulterated movies remain the best. Stop fiddling with them, George!</p>\n\n  <p><a href=\"appboy://close\">Close</a></p>\n\n</div></div>\n\n\n</body></html>"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_body_youtube_iframe.html",
    "content": "<!DOCTYPE html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n    <style>\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Medium.ttf) format(\"ttf\");\n        font-weight: 400;\n    }\n\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Regular.ttf) format(\"ttf\");\n        font-weight: 300;\n    }\n\n    * {\n        -webkit-box-sizing: border-box;\n        -moz-box-sizing: border-box;\n        box-sizing: border-box;\n    }\n\n    body {\n        background: transparent;\n        font-family: \"Roboto\", sans-serif;\n        -ms-text-size-adjust: 100%;\n        -webkit-text-size-adjust: 100%;\n        color: #333;\n        text-align: left;\n    }\n\n    .box {\n        background: #0073d5;\n        border-radius: 2px;\n        width: 100%;\n        height: 100%;\n        min-width: 220px;\n        min-height: 220px;\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n    }\n\n    a {\n        display: block;\n        text-align: center;\n        text-decoration: none;\n        text-transform: uppercase;\n        color: #fff;\n        padding: 20px;\n    }\n\n    div.relativeTopRight {\n        position: relative;\n        float: top;\n        float: right;\n    }\n\n    </style>\n</head>\n<body>\n    <div class=\"box\">\n        <div class=\"relativeTopRight\">\n            <a href=\"appboy://close\">X</a>\n        </div>\n        <iframe width=\"60%\" height=\"50%\" src=\"https://www.youtube.com/embed/_x45EB3BWqI\">\n        </iframe>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_dark_mode.html",
    "content": "<!DOCTYPE html>\n<html>\n<style>\n  :root {\n    --page-background: #fff;\n    --page-title: #333;\n    --page-text: #333;\n  }\n\n  @media screen and (prefers-color-scheme: dark) {\n    :root {\n      --page-background: #2d3239;\n      --page-title: #e9d970;\n      --page-text: #75715e;\n    }\n  }\n\n  body {\n    background: var(--page-background);\n    color: var(--page-text);\n  }\n</style>\n  <head lang=\"en\">\n    <meta charset=\"UTF-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\"/>\n  </head>\n  <body>\n    <a onclick=\"window.location='appboy://customEvent?name=htmlIAMCustomEvent&p1=p2'\">Click to log a Custom Event</a>\n    <br>\n    <br>\n    <br>\n    <br>\n    <a href=\"appboy://close\">X</a>\n  </body>\n\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_inapp_message_delayed_open.html",
    "content": "<!DOCTYPE html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\">\n    <script type=\"application/javascript\">\n      const endMs = Date.now() + 3000;\n\n      while (Date.now() < endMs) {\n        const a = 1;\n      }\n    </script>\n    <style>\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Medium.ttf) format(\"ttf\");\n        font-weight: 400;\n    }\n\n    @font-face {\n        font-family: \"Roboto\";\n        src: local('☁'), url(./Roboto-Regular.ttf) format(\"ttf\");\n        font-weight: 300;\n    }\n\n    * {\n        -webkit-box-sizing: border-box;\n        -moz-box-sizing: border-box;\n        box-sizing: border-box;\n    }\n\n    body {\n        background: transparent;\n        font-family: \"Roboto\", sans-serif;\n        -ms-text-size-adjust: 100%;\n        -webkit-text-size-adjust: 100%;\n        color: #333;\n        text-align: left;\n    }\n\n    .box {\n        background: #0073d5;\n        border-radius: 2px;\n        width: 100%;\n        height: 100%;\n        min-width: 220px;\n        min-height: 220px;\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n    }\n\n    a {\n        display: block;\n        text-align: center;\n        text-decoration: none;\n        text-transform: uppercase;\n        color: #fff;\n        padding: 20px;\n    }\n\n    div.relativeLeft {\n        position: relative;\n        top: 0px;\n        right: 0;\n        width: 0px;\n        height: 0px;\n    }\n\n    div.relativeRight {\n        position: relative;\n        float: right;\n    }\n\n    div.relativeTopRight {\n        position: relative;\n        float: top;\n        float: right;\n    }\n\n    </style>\n</head>\n<body>\n<div class=\"box\">\n    <div class=\"relativeTopRight\">\n        <a href=\"appboy://close\">X</a>\n    </div>\n    <a href=\"http://braze.com?abDeepLink=true\">Visit Appboy.com</a>\n    <a href=\"https://github.com/Appboy/appboy-android-sdk\">Visit Braze Android SDK</a>\n    <a href=\"tel://15043277269\">Call Braze</a>\n    <a href=\"mailto:droidboy@braze.com\">Email Droidboy</a>\n    <a href=\"appboy://feed\">Open the Newsfeed</a>\n    <div class=\"relativeLeft\">\n        <a href=\"https://twitter.com/braze?abButtonId=0&abExternalOpen=true\">Braze Twitter</a>\n    </div>\n    <div class=\"relativeRight\">\n        <a href=\"appboy://close?abButtonId=1\">Close</a>\n    </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/html_shark_unified.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n\n    <title>Baby Shark Title:</title>\n\n    <!-- Bootstrap core CSS -->\n    <!--  <link href=\"vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">-->\n    <link href=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c7c522eaf86420d72175a/original.css?1594653778\"\n          rel=\"stylesheet\">\n\n    <!-- Custom styles for this template -->\n    <!--  <link href=\"css/modern-business.css\" rel=\"stylesheet\">-->\n    <link href=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c7c5af578524e7799a31e/original.css?1594653786\"\n          rel=\"stylesheet\">\n    <link href=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c9e3ad0195a39dcb9e2b6/original.css?1594662458\"\n          rel=\"stylesheet\">\n    <style>\n     .margin-top-05 { margin-top: 0.5em; }\n     .margin-top-10 { margin-top: 1.0em; }\n     .margin-top-15 { margin-top: 1.5em; }\n     .margin-top-20 { margin-top: 2.0em; }\n     .margin-top-25 { margin-top: 2.5em; }\n     .margin-top-30 { margin-top: 3.0em; }\n   </style>\n\n\n</head>\n\n<body>\n\n<!-- Navigation -->\n<nav class=\"navbar fixed-top navbar-expand-lg navbar-dark bg-dark fixed-top\">\n    <div class=\"container\">\n        <a class=\"navbar-brand\">Baby Shark QA Session:</a>\n    </div>\n</nav>\n\n<!-- Page Content -->\n<div class=\"container\">\n\n\n    <div class=\"row margin-top-20 \">\n        <div class=\"col-md-12\">\n            <h2> JS TEST SECTION:</h2>\n            <hr>\n        </div>\n        <div class=\"col-md-6\">\n            <h3>JS updating UI</h3>\n            <h4 id=\"time\"></h4>\n            <p>\n                Do you see the current time? and is it updating every second?\n            </p>\n        </div>\n    </div>\n\n    <div class=\"box\">\n        <iframe width=\"80%\" height=\"90%\" src=\"https://www.youtube.com/embed/4FUPxkIq2xc\">\n        </iframe>\n    </div>\n\n\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h2> URL TEST SECTION:</h2>\n            <hr>\n        </div>\n        <div class=\"col-md-6\">\n            A youtube video link\n            <p><a href=\"https://www.youtube.com/watch?v=9M_FlkAiTSo?abDeepLink=true.\" rel=\"noopener noreferrer\" target=\"_blank\">https://www.youtube.com/watch?v=9M_FlkAiTSo?abDeepLink=true.</a></p>\n        </div>\n\n        <div class=\"col-md-6\">\n            A tiny url that takes you to a youtube video\n            <p><a href=\"https://tinyurl.com/vrxhg2p?abDeepLink=true.\" rel=\"noopener noreferrer\" target=\"_blank\">https://tinyurl.com/vrxhg2p?abDeepLink=true.</a></p>\n        </div>\n\n        <div class=\"col-md-6\">\n            A linkedIn page goes to a company.\n            <p><a href=\"https://www.linkedin.com/company/pinkfong-usa/?abDeepLink=true.\" rel=\"noopener noreferrer\" target=\"_blank\">https://www.linkedin.com/company/pinkfong-usa/?abDeepLink=true.</a></p>\n            <p>Fun Fact: The company wrote baby shark has < 10 people </p>\n        </div>\n\n        <div class=\"col-md-6\">\n            A Facebook page goes to a group.\n            <p><a href=\"https://www.facebook.com/pinkfong.official/?abDeepLink=true.\" rel=\"noopener noreferrer\" target=\"_blank\">https://www.facebook.com/pinkfong.official/?abDeepLink=true.</a></p>\n        </div>\n\n        <!--    <div class=\"col-md-6\">-->\n        <!--      An embedded youtube video-->\n        <!--      <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/BiZ0UcO3FZI?start=12\" frameborder=\"0\"-->\n        <!--              allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>-->\n        <!--    </div>-->\n    </div>\n    <hr>\n\n    <!-- Project One -->\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h2> IMAGE TEST SECTION:</h2>\n            <hr>\n        </div>\n\n        <div class=\"col-md-7\">\n            <img class=\"img-fluid rounded mb-3 mb-md-0\"\n                 src=\"https://www.freeiconspng.com/uploads/fire-leaf-clip-art-tree-hand-png-transparency--15.png\"\n                 alt=\"\">\n        </div>\n        <div class=\"col-md-5\">\n            <h3>PNG</h3>\n            <p>\n                Is the transparency rendering properly?\n            </p>\n        </div>\n    </div>\n    <!-- /.row -->\n\n    <hr>\n\n    <!-- Project Two -->\n    <div class=\"row\">\n        <div class=\"col-md-7\">\n            <img class=\"img-fluid rounded mb-3 mb-md-0\"\n                 src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/5f0c8016f578524ef299a3c5/original.jpg?1594654742\"\n                 alt=\"\">\n        </div>\n        <div class=\"col-md-5\">\n            <h3>JPG</h3>\n        </div>\n    </div>\n    <hr>\n\n    <!-- /.row -->\n    <div class=\"row\">\n        <div class=\"col-md-7\">\n            <img class=\"img-fluid rounded mb-3 mb-md-0\"\n                 src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/5b202c58fbe76a633d2920fd/original.jpeg?1528835160\"\n                 alt=\"\">\n        </div>\n        <div class=\"col-md-5\">\n            <h3>JPEG</h3>\n        </div>\n    </div>\n\n    <hr>\n\n    <!-- Project Four -->\n    <div class=\"row\">\n\n        <div class=\"col-md-7\">\n            <img class=\"img-fluid rounded mb-3 mb-md-0\"\n                 src=\"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/5f0c8017d0195a085c7eea1e/original.gif?1594654743\"\n                 alt=\"\">\n        </div>\n        <div class=\"col-md-5\">\n            <h3>GIF</h3>\n        </div>\n    </div>\n    <!-- /.row -->\n    <hr>\n\n\n    <div class=\"row\">\n\n        <div class=\"col-md-7\">\n            <a href=\"#\">\n                <img class=\"img-fluid rounded mb-3 mb-md-0\"\n                     src=\"https://cdn-staging.braze.com/appboy/communication/assets/svg_assets/files/5f0c80162eaf86427e7217ee/original.svg?1594654742\"\n                     alt=\"\">\n            </a>\n        </div>\n        <div class=\"col-md-5\">\n            <h3>SVG</h3>\n            <p> the image is all black. </p>\n        </div>\n    </div>\n    <!-- /.row -->\n    <hr>\n\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h2> Font are converted by <a href=\"https://github.com/matthewgonzalez/fontplop\" rel=\"noopener noreferrer\" target=\"_blank\">this tool</a></h2>\n            <hr>\n        </div>\n        <div class=\"col-md-6\" style=\"font-family: 'Allura Regular ttf'\">\n            <h3>TTF</h3>\n            <p>\n                Baby shark, doo, doo, doo, doo, doo, doo<br>\n                Baby shark, doo, doo, doo, doo, doo, doo<br>\n                Baby shark, doo, doo, doo, doo, doo, doo<br>\n                Baby shark<br>\n            </p>\n        </div>\n        <div class=\"col-md-6\" style=\"font-family: 'Sailec Regular otf'\">\n            <h3>OTF</h3>\n            <p>\n                Mommy shark, doo, doo, doo, doo, doo, doo<br>\n                Mommy shark, doo, doo, doo, doo, doo, doo<br>\n                Mommy shark, doo, doo, doo, doo, doo, doo<br>\n                Mommy shark<br>\n            </p>\n        </div>\n        <div class=\"col-md-6\" style=\"font-family: 'Dancing Script Regular woff'\">\n            <h3>WOFF</h3>\n            <p>\n                Daddy shark, doo, doo, doo, doo, doo, doo<br>\n                Daddy shark, doo, doo, doo, doo, doo, doo<br>\n                Daddy shark, doo, doo, doo, doo, doo, doo<br>\n                Daddy shark<br>\n            </p>\n        </div>\n        <div class=\"col-md-6\" style=\"font-family: 'Merienda One Woff2'\">\n            <h3>WOFF2</h3>\n            <p>\n                Grandma shark, doo, doo, doo, doo, doo, doo<br>\n                Grandma shark, doo, doo, doo, doo, doo, doo<br>\n                Grandma shark, doo, doo, doo, doo, doo, doo<br>\n                Grandma shark<br>\n            </p>\n        </div>\n\n        <div class=\"col-md-6\" style=\"font-family: 'Smokum Regular'\">\n            <h3>ALL</h3>\n            <p>\n                Grandpa shark, doo, doo, doo, doo, doo, doo<br>\n                Grandpa shark, doo, doo, doo, doo, doo, doo<br>\n                Grandpa shark, doo, doo, doo, doo, doo, doo<br>\n                Grandpa shark<br>\n            </p>\n        </div>\n    </div>\n    <hr>\n\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h2>MISC.:</h2>\n        </div>\n        <div class=\"col-md-6\">\n            <p>Android custom schema link: <a href=\"droidboy://preferences\" rel=\"noopener noreferrer\" target=\"_blank\"> droidboy://preferences </a></p>\n            <p>iOS custom schema link: <a href=\"stopwatch://preferences\" rel=\"noopener noreferrer\" target=\"_blank\"> stopwatch://preferences </a></p>\n            <p>Slack custom schema link: <a href=\"slack://open\"  rel=\"noopener noreferrer\" target=\"_blank\">slack://open</a></p>\n        </div>\n\n        <div class=\"col-md-6\">\n            unknown schema link: <a href=\"aasdfasdfb://preferences\" rel=\"noopener noreferrer\" target=\"_blank\"> aasdfasdfb://preferences </a>\n        </div>\n\n        <div class=\"col-md-6\">\n            <h3>button id</h3>\n            <p>abButtonId=1<a href=\"https://www.target.com/p/pinkfong-baby-shark-tablet/-/A-76167826?abButtonId=1\">\n                https://www.target.com/p/pinkfong-baby-shark-tablet/-/A-76167826?abButtonId=1\n            </a></p>\n\n            <p>abButtonId=2<a href=\"https://www.walmart.com/ip/Pinkfong-Baby-Shark-Official-Song-Doll-Daddy-Shark-By-WowWee/831703487?wmlspartner=wlpa&selectedSellerId=0&abButtonId=2\">\n                https://www.walmart.com/ip/Pinkfong-Baby-Shark-Official-Song-Doll-Daddy-Shark-By-WowWee/831703487?wmlspartner=wlpa&selectedSellerId=0&abButtonId=2\n            </a></p>\n\n            <p>body<a href=\"https://google.com\">\n                https://google.com\n            </a></p>\n\n        </div>\n    </div>\n    <hr>\n\n    <div class=\"row\">\n        <div class=\"col-md-12\">\n            <h2>EXIT SESSION</h2>\n            <a href=\"#\" onclick=\"brazeBridge.closeMessage()\"><h3>Click here to close the campaign.</h3></a>\n        </div>\n    </div>\n    <hr>\n\n</div>\n<!-- /.container -->\n\n<!-- Footer -->\n<footer class=\"py-5 bg-dark\">\n    <div class=\"container\">\n        <p class=\"m-0 text-center text-white\">Copyright &copy; Your Website 2020</p>\n    </div>\n    <!-- /.container -->\n</footer>\n\n<!-- Bootstrap core JavaScript -->\n<!--  <script src=\"vendor/jquery/jquery.min.js\"></script>-->\n<script src=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c7c992eaf8641d3721783/original.js?1594653849\"></script>\n<!--  <script src=\"vendor/bootstrap/js/bootstrap.bundle.min.js\"></script>-->\n<script src=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c7c4dd0195a07867ee95c/original.js?1594653773\"></script>\n<script src=\"https://cdn-staging.braze.com/appboy/communication/assets/code_assets/files/5f0c814d2eaf864190721833/original.js?1594655053\"></script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "droidboy/src/main/assets/modal_inapp_message_with_dark_theme.json",
    "content": "{\n  \"message\": \"This is a test modal in-app message\",\n  \"duration\": 5000,\n  \"slide_from\": \"bottom\",\n  \"extras\": {\n    \"one\": \"1\",\n    \"two\": \"2\",\n    \"three\": \"3\"\n  },\n  \"click_action\": \"uri\",\n  \"message_close\": \"swipe\",\n  \"uri\": \"http://www.sports.yahoo.com/\",\n  \"bg_color\": 0xffffffff,\n  \"icon_color\": 0xffffffff,\n  \"icon_bg_color\": 0xff0000ff,\n  \"text_color\": 0xff0000ff,\n  \"header\": \"I'm a header\",\n  \"header_text_color\": 0xff0000ff,\n  \"close_btn_color\": 0xff000000,\n  \"orientation\": \"PORTRAIT\",\n  \"type\": \"MODAL\",\n  \"icon\": \"\\uffff\",\n  \"image_url\": \"https://appboy-images.com/appboy/communication/assets/image_assets/images/57c7610856ec31384ef556bf/original.?1472684296\",\n  \"btns\": [\n    {\n      \"text\": \"button one\",\n      \"bg_color\": 0xff468ae3,\n      \"text_color\": 0xffe8fc49,\n      \"id\": 0,\n      \"use_webview\": true,\n      \"click_action\": \"none\"\n    },\n    {\n      \"text\": \"button two\",\n      \"bg_color\": 0xff468ae3,\n      \"text_color\": 0xffe8fc49,\n      \"id\": 0,\n      \"use_webview\": true,\n      \"click_action\": \"none\"\n    }\n  ],\n  \"themes\" : {\n    \"dark\" : {\n      \"close_btn_color\": 0xddedf5f0,\n      \"icon_color\": 0x00d3c0d3,\n      \"icon_bg_color\": 0xdead2bad,\n      \"bg_color\": 0xffa18f8f,\n      \"text_color\": 0xb0c0c9ca,\n      \"header_text_color\": 0xbbe7e8e6,\n      \"frame_color\": 0xc0363030,\n      \"btns\" : [\n        {\n          \"bg_color\": 0xff969090,\n          \"text_color\": 0xfff7f0f0,\n          \"border_color\": 0xff524949\n        },\n        {\n          \"bg_color\": 0xff969090,\n          \"text_color\": 0xfff7f0f0,\n          \"border_color\": 0xff524949\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomBrazeDeeplinkHandler.java",
    "content": "package com.appboy.sample;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.braze.Constants;\nimport com.braze.enums.Channel;\nimport com.appboy.sample.activity.DroidBoyActivity;\nimport com.braze.IBrazeDeeplinkHandler;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.StringUtils;\nimport com.braze.ui.BrazeDeeplinkHandler;\nimport com.braze.ui.actions.NewsfeedAction;\nimport com.braze.ui.actions.UriAction;\n\npublic class CustomBrazeDeeplinkHandler implements IBrazeDeeplinkHandler {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(CustomBrazeDeeplinkHandler.class);\n\n  @Override\n  public void gotoNewsFeed(Context context, NewsfeedAction newsfeedAction) {\n    Intent intent = new Intent(context, DroidBoyActivity.class);\n    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);\n    intent.putExtras(newsfeedAction.getExtras());\n    intent.putExtra(context.getResources().getString(R.string.source_key), Constants.BRAZE);\n    intent.putExtra(context.getResources().getString(R.string.destination_view), context.getResources().getString(R.string.feed_key));\n    context.startActivity(intent);\n  }\n\n  @Override\n  public void gotoUri(Context context, UriAction uriAction) {\n    String uri = uriAction.getUri().toString();\n    if (!StringUtils.isNullOrBlank(uri) && uri.matches(context.getString(R.string.youtube_regex))) {\n      uriAction.setUseWebView(false);\n    }\n\n    CustomUriAction customUriAction = new CustomUriAction(uriAction);\n    customUriAction.execute(context);\n  }\n\n  @Override\n  public int getIntentFlags(IntentFlagPurpose intentFlagPurpose) {\n    return new BrazeDeeplinkHandler().getInstance().getIntentFlags(intentFlagPurpose);\n  }\n\n  @Nullable\n  @Override\n  public UriAction createUriActionFromUrlString(String url, Bundle extras, boolean openInWebView, Channel channel) {\n    return BrazeDeeplinkHandler.getInstance().createUriActionFromUrlString(url, extras, openInWebView, channel);\n  }\n\n  @Nullable\n  @Override\n  public UriAction createUriActionFromUri(Uri uri, Bundle extras, boolean openInWebView, Channel channel) {\n    return BrazeDeeplinkHandler.getInstance().createUriActionFromUri(uri, extras, openInWebView, channel);\n  }\n\n  public static class CustomUriAction extends UriAction {\n\n    public CustomUriAction(@NonNull UriAction uriAction) {\n      super(uriAction);\n    }\n\n    @Override\n    protected void openUriWithActionView(Context context, Uri uri, Bundle extras) {\n      Intent intent = getActionViewIntent(context, uri, extras);\n      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);\n      try {\n        context.startActivity(intent);\n      } catch (Exception e) {\n        BrazeLogger.e(TAG, \"Failed to handle uri \" + uri + \" with extras: \" + extras, e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomFeedClickActionListener.java",
    "content": "package com.appboy.sample;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\n\nimport com.braze.models.cards.Card;\nimport com.braze.ui.feed.listeners.IFeedClickActionListener;\nimport com.braze.support.StringUtils;\nimport com.braze.ui.actions.IAction;\n\npublic class CustomFeedClickActionListener implements IFeedClickActionListener {\n  @Override\n  public boolean onFeedCardClicked(Context context, Card card, IAction cardAction) {\n    if (!StringUtils.isNullOrBlank(card.getUrl()) && card.getUrl().matches(context.getString(R.string.youtube_regex))) {\n      Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(card.getUrl()));\n      context.startActivity(intent);\n      return true;\n    } else {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomHtmlInAppMessageActionListener.java",
    "content": "package com.appboy.sample;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.widget.Toast;\n\nimport com.braze.models.inappmessage.IInAppMessage;\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager;\nimport com.braze.ui.inappmessage.listeners.IHtmlInAppMessageActionListener;\n\n/**\n * Closes the current In App Message and displays a toast\n */\npublic class CustomHtmlInAppMessageActionListener implements IHtmlInAppMessageActionListener {\n\n  private final Context mContext;\n\n  public CustomHtmlInAppMessageActionListener(Context context) {\n    mContext = context;\n  }\n\n  @Override\n  public void onCloseClicked(IInAppMessage inAppMessage, String url, Bundle queryBundle) {\n    Toast.makeText(mContext, \"HTML In App Message closed\", Toast.LENGTH_LONG).show();\n    BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(false);\n  }\n\n  @Override\n  public boolean onCustomEventFired(IInAppMessage inAppMessage, String url, Bundle queryBundle) {\n    Toast.makeText(mContext, \"Custom event fired. Ignoring.\", Toast.LENGTH_LONG).show();\n    return true;\n  }\n\n  @Override\n  public boolean onNewsfeedClicked(IInAppMessage inAppMessage, String url, Bundle queryBundle) {\n    Toast.makeText(mContext, \"Newsfeed button pressed. Ignoring.\", Toast.LENGTH_LONG).show();\n    BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(false);\n    return true;\n  }\n\n  @Override\n  public boolean onOtherUrlAction(IInAppMessage inAppMessage, String url, Bundle queryBundle) {\n    Toast.makeText(mContext, \"Custom url pressed: \" + url + \" . Ignoring\", Toast.LENGTH_LONG).show();\n    BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(false);\n    return true;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomInAppMessage.java",
    "content": "package com.appboy.sample;\n\nimport com.braze.models.inappmessage.InAppMessageModal;\n\npublic class CustomInAppMessage extends InAppMessageModal {\n\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomInAppMessageAnimationFactory.java",
    "content": "package com.appboy.sample;\n\nimport android.view.animation.AccelerateInterpolator;\nimport android.view.animation.AlphaAnimation;\nimport android.view.animation.Animation;\nimport android.view.animation.DecelerateInterpolator;\n\nimport com.braze.models.inappmessage.IInAppMessage;\nimport com.braze.ui.inappmessage.IInAppMessageAnimationFactory;\n\npublic class CustomInAppMessageAnimationFactory implements IInAppMessageAnimationFactory {\n\n  @Override\n  public Animation getOpeningAnimation(IInAppMessage inAppMessage) {\n    Animation animation = new AlphaAnimation(0, 1);\n    animation.setInterpolator(new AccelerateInterpolator());\n    animation.setDuration(2000L);\n    return animation;\n  }\n\n  @Override\n  public Animation getClosingAnimation(IInAppMessage inAppMessage) {\n    Animation animation = new AlphaAnimation(1, 0);\n    animation.setInterpolator(new DecelerateInterpolator());\n    animation.setDuration(2000L);\n    return animation;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomInAppMessageManagerListener.java",
    "content": "package com.appboy.sample;\n\nimport android.app.Activity;\nimport android.view.View;\nimport android.widget.Toast;\n\nimport com.braze.models.inappmessage.IInAppMessage;\nimport com.braze.models.inappmessage.MessageButton;\nimport com.braze.ui.inappmessage.InAppMessageCloser;\nimport com.braze.ui.inappmessage.InAppMessageOperation;\nimport com.braze.ui.inappmessage.listeners.IInAppMessageManagerListener;\n\nimport java.util.Map;\n\npublic class CustomInAppMessageManagerListener implements IInAppMessageManagerListener {\n  private final Activity mActivity;\n\n  public CustomInAppMessageManagerListener(Activity activity) {\n    mActivity = activity;\n  }\n\n  @Override\n  public InAppMessageOperation beforeInAppMessageDisplayed(IInAppMessage inAppMessage) {\n    return InAppMessageOperation.DISPLAY_NOW;\n  }\n\n  @Override\n  public boolean onInAppMessageClicked(IInAppMessage inAppMessage, InAppMessageCloser inAppMessageCloser) {\n    Toast.makeText(mActivity, \"The click was ignored.\", Toast.LENGTH_LONG).show();\n\n    // Closing should not be animated if transitioning to a new activity.\n    // If remaining in the same activity, closing should be animated.\n    inAppMessageCloser.close(true);\n    return true;\n  }\n\n  @Override\n  public boolean onInAppMessageButtonClicked(IInAppMessage inAppMessage, MessageButton button, InAppMessageCloser inAppMessageCloser) {\n    Toast.makeText(mActivity, \"The button click was ignored.\", Toast.LENGTH_LONG).show();\n\n    // Closing should not be animated if transitioning to a new activity.\n    // If remaining in the same activity, closing should be animated.\n    inAppMessageCloser.close(true);\n    return true;\n  }\n\n  @Override\n  public void onInAppMessageDismissed(IInAppMessage inAppMessage) {\n    if (inAppMessage.getExtras() != null && !inAppMessage.getExtras().isEmpty()) {\n      Map<String, String> extras = inAppMessage.getExtras();\n      StringBuilder keyValuePairs = new StringBuilder(\"Dismissed in-app message with extras payload containing [\");\n      for (String key : extras.keySet()) {\n        keyValuePairs.append(\" '\").append(key).append(\" = \").append(extras.get(key)).append('\\'');\n      }\n      keyValuePairs.append(']');\n      Toast.makeText(mActivity, keyValuePairs.toString(), Toast.LENGTH_LONG).show();\n    } else {\n      Toast.makeText(mActivity, \"The in-app message was dismissed.\", Toast.LENGTH_LONG).show();\n    }\n  }\n\n  @Override\n  public void beforeInAppMessageViewOpened(View inAppMessageView, IInAppMessage inAppMessage) { }\n\n  @Override\n  public void afterInAppMessageViewOpened(View inAppMessageView, IInAppMessage inAppMessage) { }\n\n  @Override\n  public void beforeInAppMessageViewClosed(View inAppMessageView, IInAppMessage inAppMessage) { }\n\n  @Override\n  public void afterInAppMessageViewClosed(IInAppMessage inAppMessage) { }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomInAppMessageView.java",
    "content": "package com.appboy.sample;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.LayerDrawable;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.view.WindowInsetsCompat;\n\nimport com.braze.ui.inappmessage.utils.InAppMessageViewUtils;\nimport com.braze.ui.inappmessage.views.IInAppMessageView;\nimport com.braze.ui.support.ViewUtils;\n\npublic class CustomInAppMessageView extends RelativeLayout implements IInAppMessageView {\n\n  public CustomInAppMessageView(Context context, AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public void setMessageBackgroundColor(int color) {\n    LayerDrawable layerDrawable = (LayerDrawable) findViewById(R.id.inappmessage).getBackground();\n    InAppMessageViewUtils.setDrawableColor(layerDrawable.findDrawableByLayerId(R.id.inappmessage_background), color);\n  }\n\n  public void setMessageTextColor(int color) {\n    InAppMessageViewUtils.setTextViewColor(getMessageTextView(), color);\n  }\n\n  public void setMessage(String text) {\n    getMessageTextView().setText(text);\n  }\n\n  public void setMessageImage(Bitmap bitmap) {\n    InAppMessageViewUtils.setImage(bitmap, getMessageImageView());\n  }\n\n  public void setMessageIcon(String icon, int iconColor, int iconBackgroundColor) {\n    InAppMessageViewUtils.setIcon(getContext(), icon, iconColor, iconBackgroundColor, getMessageIconView());\n  }\n\n  public void resetMessageMargins() {\n    if (getMessageImageView().getDrawable() == null) {\n      ViewUtils.removeViewFromParent(getMessageImageView());\n    }\n    if (getMessageIconView().getText().length() == 0) {\n      ViewUtils.removeViewFromParent(getMessageIconView());\n    }\n  }\n\n  public View getMessageClickableView() {\n    return this;\n  }\n\n  @Override\n  public void applyWindowInsets(@NonNull WindowInsetsCompat insets) {\n    // Does nothing\n  }\n\n  public TextView getMessageTextView() {\n    return findViewById(R.id.inappmessage_message);\n  }\n\n  public ImageView getMessageImageView() {\n    return findViewById(R.id.inappmessage_image);\n  }\n\n  public TextView getMessageIconView() {\n    return findViewById(R.id.inappmessage_icon);\n  }\n\n  @Override\n  public boolean getHasAppliedWindowInsets() {\n    return false;\n  }\n\n  @Override\n  public void setHasAppliedWindowInsets(boolean hasAppliedWindowInsets) {\n\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/CustomInAppMessageViewFactory.java",
    "content": "package com.appboy.sample;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport com.braze.models.inappmessage.IInAppMessage;\nimport com.braze.models.inappmessage.IInAppMessageWithImage;\nimport com.braze.ui.inappmessage.IInAppMessageViewFactory;\n\npublic class CustomInAppMessageViewFactory implements IInAppMessageViewFactory {\n  @SuppressLint(\"InflateParams\")\n  @Override\n  public View createInAppMessageView(@NonNull Activity activity, @NonNull IInAppMessage inAppMessage) {\n    CustomInAppMessageView inAppMessageView = (CustomInAppMessageView) activity.getLayoutInflater().inflate(R.layout.custom_inappmessage, null);\n\n    inAppMessageView.setMessageBackgroundColor(inAppMessage.getBackgroundColor());\n    inAppMessageView.setMessage(inAppMessage.getMessage());\n    inAppMessageView.setMessageTextColor(inAppMessage.getMessageTextColor());\n    inAppMessageView.setMessageIcon(inAppMessage.getIcon(), inAppMessage.getIconBackgroundColor(), inAppMessage.getIconColor());\n    if (inAppMessage instanceof IInAppMessageWithImage) {\n      inAppMessageView.setMessageImage(((IInAppMessageWithImage) inAppMessage).getBitmap());\n    }\n    inAppMessageView.resetMessageMargins();\n\n    return inAppMessageView;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/DroidboyApplication.kt",
    "content": "package com.appboy.sample\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Application\nimport android.app.NotificationChannel\nimport android.app.NotificationChannelGroup\nimport android.app.NotificationManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.content.pm.ShortcutInfo\nimport android.content.pm.ShortcutManager\nimport android.graphics.Color\nimport android.graphics.drawable.Icon\nimport android.net.TrafficStats\nimport android.net.Uri\nimport android.os.Build\nimport android.os.StrictMode\nimport android.os.StrictMode.VmPolicy\nimport android.util.Log\nimport android.webkit.WebView\nimport androidx.annotation.RequiresApi\nimport androidx.multidex.MultiDex\nimport com.appboy.sample.util.BrazeActionTestingUtil\nimport com.appboy.sample.util.ContentCardsTestingUtil\nimport com.braze.Braze\nimport com.braze.BrazeActivityLifecycleCallbackListener\nimport com.braze.BrazeInternal\nimport com.braze.configuration.BrazeConfig\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.enums.BrazeSdkMetadata\nimport com.braze.events.BrazeSdkAuthenticationErrorEvent\nimport com.braze.support.BrazeLogger\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.PackageUtils\nimport com.braze.support.getPrettyPrintedString\nimport com.braze.support.hasPermission\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.google.firebase.crashlytics.FirebaseCrashlytics\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.json.JSONObject\nimport java.io.OutputStreamWriter\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.*\n\nclass DroidboyApplication : Application() {\n    private var isSdkAuthEnabled: Boolean = false\n\n    override fun onCreate() {\n        super.onCreate()\n        if (BuildConfig.DEBUG && BuildConfig.STRICTMODE_ENABLED) {\n            activateStrictMode()\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            WebView.setWebContentsDebuggingEnabled(true)\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {\n            setupChatDynamicShortcut()\n        }\n\n        registerActivityLifecycleCallbacks(BrazeActivityLifecycleCallbackListener())\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            if (this.hasPermission(Manifest.permission.POST_NOTIFICATIONS)) {\n                setupNotificationChannels()\n            } else {\n                // Ask for the push prompt via a Braze Action #dogfooding\n                brazelog { \"BrazeInApp adding push prompt\" }\n                val brazeInAppMessageManager = BrazeInAppMessageManager.getInstance()\n                brazeInAppMessageManager\n                    .addInAppMessage(BrazeActionTestingUtil.getPushPromptInAppMessageModal(this.applicationContext))\n                brazeInAppMessageManager.requestDisplayInAppMessage()\n            }\n        } else {\n            setupNotificationChannels()\n        }\n        setupFirebaseCrashlytics()\n        BrazeLogger.logLevel = applicationContext.getSharedPreferences(getString(R.string.log_level_dialog_title), MODE_PRIVATE)\n            .getInt(getString(R.string.current_log_level), Log.VERBOSE)\n        val sharedPreferences = applicationContext.getSharedPreferences(getString(R.string.shared_prefs_location), Context.MODE_PRIVATE)\n\n        Braze.configure(this, null)\n        val brazeConfigBuilder = BrazeConfig.Builder()\n        brazeConfigBuilder.setSdkMetadata(EnumSet.of(BrazeSdkMetadata.MANUAL))\n        setOverrideApiKeyIfConfigured(sharedPreferences, brazeConfigBuilder)\n        setOverrideEndpointIfConfigured(sharedPreferences, brazeConfigBuilder)\n        setMinTriggerIntervalIfConfigured(sharedPreferences, brazeConfigBuilder)\n        isSdkAuthEnabled = setSdkAuthIfConfigured(sharedPreferences, brazeConfigBuilder)\n        Braze.configure(this, brazeConfigBuilder.build())\n        Braze.addSdkMetadata(this, EnumSet.of(BrazeSdkMetadata.BRANCH))\n\n        if (isSdkAuthEnabled) {\n            Braze.getInstance(applicationContext).subscribeToSdkAuthenticationFailures { message: BrazeSdkAuthenticationErrorEvent ->\n                brazelog { \"Got sdk auth error message $message\" }\n                message.userId?.let { setNewSdkAuthToken(it) }\n            }\n            // Fire off an update to start off\n            Braze.getInstance(applicationContext).currentUser?.userId?.let { setNewSdkAuthToken(it) }\n        }\n\n        Braze.getInstance(applicationContext).subscribeToPushNotificationEvents { event ->\n            brazelog {\n                \"\"\"\n                    Got braze push notification event $event \n                    with title '${event.notificationPayload.titleText}'\n                    and deeplink '${event.notificationPayload.deeplink}'\n                \"\"\".trimIndent()\n            }\n        }\n        listenForSpecialTabFeatureFlag()\n    }\n\n    override fun attachBaseContext(context: Context?) {\n        super.attachBaseContext(context)\n        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) {\n            MultiDex.install(this)\n        }\n    }\n\n    /**\n     * Calls [Braze.changeUser] with a new SDK Auth token, if available. If no\n     * token is available, just calls [Braze.changeUser] without a new SDK Auth token.\n     */\n    fun changeUserWithNewSdkAuthToken(userId: String) {\n        BrazeCoroutineScope.launch {\n            val token = getSdkAuthToken(userId)\n            if (token != null) {\n                Braze.getInstance(applicationContext).changeUser(userId, token)\n            } else {\n                Braze.getInstance(applicationContext).changeUser(userId)\n            }\n        }\n    }\n\n    private fun setNewSdkAuthToken(userId: String) {\n        BrazeCoroutineScope.launch {\n            val token = getSdkAuthToken(userId) ?: return@launch\n            Braze.getInstance(applicationContext).setSdkAuthenticationSignature(token)\n        }\n    }\n\n    private suspend fun getSdkAuthToken(userId: String): String? {\n        if (!isSdkAuthEnabled) return null\n\n        try {\n            return withContext(BrazeCoroutineScope.coroutineContext) {\n                brazelog(TAG) { \"Making new SDK Auth token request for user: '$userId'\" }\n\n                val url = URL(BuildConfig.SDK_AUTH_ENDPOINT)\n                val payload = JSONObject()\n                    .put(\n                        \"data\",\n                        JSONObject()\n                            .put(\"user_id\", userId)\n                    )\n\n                with(url.openConnection() as HttpURLConnection) {\n                    TrafficStats.setThreadStatsTag(1337)\n                    requestMethod = \"POST\"\n                    addRequestProperty(\"Content-Type\", \"application/json\")\n                    addRequestProperty(\"Accept\", \"application/json\")\n\n                    OutputStreamWriter(outputStream).use { out -> out.write(payload.toString()) }\n\n                    val responseJson = JSONObject(inputStream.bufferedReader().readText())\n                    brazelog(TAG) { \"SDK auth callback got response: ${responseJson.getPrettyPrintedString()}\" }\n                    return@withContext responseJson.optJSONObject(\"data\")?.optString(\"token\")\n                }\n            }\n        } catch (e: Exception) {\n            brazelog(TAG, E, e) { \"Failed to get SDK Auth token\" }\n            return null\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.N_MR1)\n    private fun setupChatDynamicShortcut() {\n        val builder = ShortcutInfo.Builder(this, \"droidboy_dynamic_shortcut_chat_id\")\n            .setShortLabel(\"Braze Chat\")\n            .setLongLabel(\"Conversational Push\")\n            .setIcon(Icon.createWithResource(this, android.R.drawable.ic_menu_send))\n            .setIntent(\n                Intent(\n                    Intent.ACTION_VIEW,\n                    Uri.parse(\"https://www.braze.com?dynamicshortcut=true\")\n                )\n            )\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            builder.setLongLived(true)\n        }\n\n        val shortcutManager: ShortcutManager = getSystemService(ShortcutManager::class.java)\n        shortcutManager.dynamicShortcuts = listOf(builder.build())\n    }\n\n    private fun setupNotificationChannels() {\n        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n        createNotificationGroup(notificationManager, R.string.droidboy_notification_group_01_id, R.string.droidboy_notification_group_01_name)\n        createNotificationChannel(\n            notificationManager, R.string.droidboy_notification_channel_01_id, R.string.droidboy_notification_channel_messages_name,\n            R.string.droidboy_notification_channel_messages_desc, R.string.droidboy_notification_group_01_id\n        )\n        createNotificationChannel(\n            notificationManager, R.string.droidboy_notification_channel_02_id, R.string.droidboy_notification_channel_matches_name,\n            R.string.droidboy_notification_channel_matches_desc, R.string.droidboy_notification_group_01_id\n        )\n        createNotificationChannel(\n            notificationManager, R.string.droidboy_notification_channel_03_id, R.string.droidboy_notification_channel_offers_name,\n            R.string.droidboy_notification_channel_offers_desc, R.string.droidboy_notification_group_01_id\n        )\n        createNotificationChannel(\n            notificationManager, R.string.droidboy_notification_channel_04_id, R.string.droidboy_notification_channel_recommendations_name,\n            R.string.droidboy_notification_channel_recommendations_desc, R.string.droidboy_notification_group_01_id\n        )\n    }\n\n    @SuppressLint(\"NewApi\")\n    private fun createNotificationGroup(notificationManager: NotificationManager, idResource: Int, nameResource: Int) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            notificationManager.createNotificationChannelGroup(\n                NotificationChannelGroup(\n                    getString(idResource),\n                    getString(nameResource)\n                )\n            )\n        }\n    }\n\n    @SuppressLint(\"NewApi\")\n    private fun createNotificationChannel(notificationManager: NotificationManager, idResource: Int, nameResource: Int, descResource: Int, groupResource: Int) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            val channel = NotificationChannel(\n                getString(idResource),\n                getString(nameResource),\n                NotificationManager.IMPORTANCE_LOW\n            )\n            channel.description = getString(descResource)\n            channel.enableLights(true)\n            channel.lightColor = Color.RED\n            channel.group = getString(groupResource)\n            channel.enableVibration(true)\n            channel.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)\n            notificationManager.createNotificationChannel(channel)\n        }\n    }\n\n    @SuppressLint(\"NewApi\")\n    private fun activateStrictMode() {\n        val threadPolicyBuilder = StrictMode.ThreadPolicy.Builder()\n            .detectAll()\n            .penaltyLog()\n\n        // We are explicitly not detecting detectLeakedClosableObjects(), detectLeakedSqlLiteObjects(), and detectUntaggedSockets()\n        // The okhttp library used on most https calls trips the detectUntaggedSockets() check\n        // com.google.android.gms.internal trips both the detectLeakedClosableObjects() and detectLeakedSqlLiteObjects() checks\n        val vmPolicyBuilder = VmPolicy.Builder()\n            .detectAll()\n            .penaltyLog()\n\n        // Note that some detections require a specific sdk version or higher to enable.\n        vmPolicyBuilder.detectLeakedRegistrationObjects()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {\n            vmPolicyBuilder.detectFileUriExposure()\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            vmPolicyBuilder.detectCleartextNetwork()\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            vmPolicyBuilder.detectContentUriWithoutPermission()\n            vmPolicyBuilder.detectUntaggedSockets()\n        }\n        StrictMode.setThreadPolicy(threadPolicyBuilder.build())\n        StrictMode.setVmPolicy(vmPolicyBuilder.build())\n    }\n\n    private fun setOverrideApiKeyIfConfigured(sharedPreferences: SharedPreferences, config: BrazeConfig.Builder) {\n        val overrideApiKey = sharedPreferences.getString(OVERRIDE_API_KEY_PREF_KEY, null)\n        if (!overrideApiKey.isNullOrBlank()) {\n            brazelog(I) { \"Override API key found, configuring Braze with override key $overrideApiKey.\" }\n            config.setApiKey(overrideApiKey)\n            overrideApiKeyInUse = overrideApiKey\n        }\n    }\n\n    private fun setOverrideEndpointIfConfigured(sharedPreferences: SharedPreferences, config: BrazeConfig.Builder) {\n        val overrideEndpoint = sharedPreferences.getString(OVERRIDE_ENDPOINT_PREF_KEY, null)\n        if (!overrideEndpoint.isNullOrBlank()) {\n            brazelog(I) { \"Override endpoint found, configuring Braze with override endpoint $overrideEndpoint.\" }\n            config.setCustomEndpoint(overrideEndpoint)\n        }\n    }\n\n    private fun setMinTriggerIntervalIfConfigured(sharedPreferences: SharedPreferences, config: BrazeConfig.Builder) {\n        val minIntervalString = sharedPreferences.getString(MIN_TRIGGER_INTERVAL_KEY, null)\n        if (!minIntervalString.isNullOrBlank()) {\n            val minTriggerInterval = minIntervalString.toIntOrNull() ?: return\n            brazelog(I) { \"Min trigger interval found, configuring Braze with minimum interval $minTriggerInterval seconds.\" }\n            config.setTriggerActionMinimumTimeIntervalSeconds(minTriggerInterval)\n        }\n    }\n\n    private fun setSdkAuthIfConfigured(sharedPreferences: SharedPreferences, config: BrazeConfig.Builder): Boolean {\n        // Default to true for testing dogfood purposes\n        val isOverridingSdkAuth = sharedPreferences.getBoolean(ENABLE_SDK_AUTH_PREF_KEY, true)\n        config.setIsSdkAuthenticationEnabled(isOverridingSdkAuth)\n        return isOverridingSdkAuth\n    }\n\n    private fun setupFirebaseCrashlytics() {\n        // Only enable crash logging for the play store deployed released builds\n        val firebaseCrashlytics = FirebaseCrashlytics.getInstance()\n        firebaseCrashlytics.setCrashlyticsCollectionEnabled(BuildConfig.IS_DROIDBOY_RELEASE_BUILD)\n        firebaseCrashlytics.setCustomKey(\"build_time\", BuildConfig.BUILD_TIME)\n        firebaseCrashlytics.setCustomKey(\"version_code\", BuildConfig.VERSION_CODE)\n        firebaseCrashlytics.setCustomKey(\"version_name\", BuildConfig.VERSION_NAME)\n    }\n\n    /**\n     * Listens for any important Feature Flag updates to this application.\n     */\n    private fun listenForSpecialTabFeatureFlag() {\n        Braze.getInstance(applicationContext).subscribeToFeatureFlagsUpdates {\n            val specialTabFeatureFlagId = \"helpful_office_tool_droidboy_tab\"\n            val specialTabFlag = Braze.getInstance(applicationContext).getFeatureFlag(specialTabFeatureFlagId)\n\n            if (specialTabFlag.enabled) {\n                val imageUrl = specialTabFlag.getStringProperty(\"image_url\")\n                    ?: \"https://raw.githubusercontent.com/Appboy/appboy-android-sdk/master/braze-logo.png\"\n                val title = specialTabFlag.getStringProperty(\"helpful_title\") ?: \"title\"\n                val desc = specialTabFlag.getStringProperty(\"helpful_description\") ?: \"description\"\n                val card = ContentCardsTestingUtil.createCaptionedImageCardJson(specialTabFlag.id, title, desc, imageUrl)\n\n                // Add our data to the Content Card feed\n                Braze.getInstance(applicationContext).getCurrentUser { brazeUser ->\n                    BrazeInternal.addSerializedContentCardToStorage(applicationContext, card.toString(), brazeUser.userId)\n                }\n            } else {\n                // Remove the card if disabled\n                val card = ContentCardsTestingUtil.getRemovedCardJson(specialTabFlag.id)\n                Braze.getInstance(applicationContext).getCurrentUser { brazeUser ->\n                    BrazeInternal.addSerializedContentCardToStorage(applicationContext, card.toString(), brazeUser.userId)\n                }\n            }\n        }\n    }\n\n    companion object {\n        private val TAG = BrazeLogger.getBrazeLogTag(DroidboyApplication::class.java)\n        private var overrideApiKeyInUse: String? = null\n        const val OVERRIDE_API_KEY_PREF_KEY = \"override_api_key\"\n        const val OVERRIDE_ENDPOINT_PREF_KEY = \"override_endpoint_url\"\n        const val ENABLE_SDK_AUTH_PREF_KEY = \"enable_sdk_auth_if_present_pref_key\"\n        const val MIN_TRIGGER_INTERVAL_KEY = \"min_trigger_interval\"\n\n        @JvmStatic\n        fun getApiKeyInUse(context: Context): String? {\n            return if (!overrideApiKeyInUse.isNullOrBlank()) {\n                overrideApiKeyInUse\n            } else {\n                // Check if the api key is in resources\n                readStringResourceValue(context, \"com_braze_api_key\", \"NO-API-KEY-SET\")\n            }\n        }\n\n        private fun readStringResourceValue(context: Context, key: String?, defaultValue: String): String {\n            return try {\n                if (key == null) {\n                    return defaultValue\n                }\n                val resId = context.resources.getIdentifier(key, \"string\", PackageUtils.getResourcePackageName(context))\n                if (resId == 0) {\n                    brazelog {\n                        \"Unable to find the xml string value with key $key. \" +\n                            \"Using default value '$defaultValue'.\"\n                    }\n                    defaultValue\n                } else {\n                    context.resources.getString(resId)\n                }\n            } catch (ignored: Exception) {\n                brazelog {\n                    \"Unexpected exception retrieving the xml string configuration\" +\n                        \" value with key $key. Using default value $defaultValue'.\"\n                }\n                defaultValue\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/DroidboyEndpointProvider.java",
    "content": "package com.appboy.sample;\n\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\n\nimport com.braze.IBrazeEndpointProvider;\n\n/**\n * An {@link IBrazeEndpointProvider} that sets an override endpoint if given.\n */\npublic class DroidboyEndpointProvider implements IBrazeEndpointProvider {\n  private final Uri mEndpointUri;\n  private final String mEndpoint;\n\n  public DroidboyEndpointProvider(@NonNull String endpoint) {\n    mEndpoint = endpoint;\n    mEndpointUri = Uri.parse(endpoint);\n  }\n\n  public Uri getApiEndpoint(Uri brazeEndpoint) {\n    final Uri.Builder builder = brazeEndpoint.buildUpon();\n\n    if (mEndpointUri.getScheme().equals(\"http\")) {\n      builder.scheme(\"http\");\n    }\n\n    if (mEndpointUri.getEncodedAuthority() != null) {\n      builder.encodedAuthority(mEndpointUri.getEncodedAuthority());\n    } else {\n      builder.encodedAuthority(mEndpoint);\n    }\n    return builder.build();\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/DroidboyNotificationFactory.java",
    "content": "package com.appboy.sample;\n\nimport android.app.Notification;\nimport android.provider.Settings;\n\nimport androidx.core.app.NotificationCompat;\n\nimport com.braze.models.push.BrazeNotificationPayload;\nimport com.braze.IBrazeNotificationFactory;\nimport com.braze.push.BrazeNotificationFactory;\n\npublic class DroidboyNotificationFactory implements IBrazeNotificationFactory {\n\n  @Override\n  public Notification createNotification(BrazeNotificationPayload brazeNotificationPayload) {\n    NotificationCompat.Builder notificationBuilder = BrazeNotificationFactory.populateNotificationBuilder(brazeNotificationPayload);\n    notificationBuilder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);\n    return notificationBuilder.build();\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/FeedCategoriesFragment.java",
    "content": "package com.appboy.sample;\n\nimport android.app.Activity;\nimport android.app.AlertDialog;\nimport android.app.Dialog;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.widget.ListView;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.braze.enums.CardCategory;\nimport com.braze.support.BrazeLogger;\n\nimport java.util.Arrays;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Locale;\n\n@SuppressWarnings({\"PMD.FieldDeclarationsShouldBeAtStartOfClass\", \"deprecation\"})\npublic class FeedCategoriesFragment extends DialogFragment {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(FeedCategoriesFragment.class);\n  public static final String CATEGORIES_STRING = \"categories\";\n\n  /**\n   * The activity that creates an instance of this dialog fragment must\n   * implement this interface in order to receive event callbacks.\n   * Each method passes the DialogFragment in case the host needs to query it.\n   */\n  public interface NoticeDialogListener {\n    void onDialogPositiveClick(FeedCategoriesFragment dialog);\n  }\n\n  public EnumSet<CardCategory> selectedCategories;\n\n  // Use this instance of the interface to deliver action events\n  NoticeDialogListener mListener;\n  boolean[] mCategoryIsChecked;\n\n  static final String[] CATEGORIES = {\"all\", CardCategory.ADVERTISING.toString(), CardCategory.ANNOUNCEMENTS.toString(), CardCategory.NEWS.toString(), CardCategory.SOCIAL.toString()};\n\n  public static FeedCategoriesFragment newInstance(EnumSet<CardCategory> categories) {\n    FeedCategoriesFragment categoriesFragment = new FeedCategoriesFragment();\n\n    Bundle args = new Bundle();\n    args.putSerializable(CATEGORIES_STRING, categories);\n    categoriesFragment.setArguments(args);\n\n    return categoriesFragment;\n  }\n\n  // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener\n  @Override\n  public void onAttach(Activity activity) {\n    super.onAttach(activity);\n    // Verify that the host activity implements the callback interface\n    try {\n      // Instantiate the NoticeDialogListener so we can send events to the host\n      mListener = (NoticeDialogListener) activity;\n    } catch (ClassCastException e) {\n      // The activity doesn't implement the interface, throw exception\n      throw new ClassCastException(activity.toString()\n          + \" must implement NoticeDialogListener\");\n    }\n  }\n\n  @NonNull\n  @Override\n  public Dialog onCreateDialog(Bundle savedInstanceState) {\n    selectedCategories = (EnumSet<CardCategory>)getArguments().getSerializable(CATEGORIES_STRING);\n    mCategoryIsChecked = getBooleansFromEnumSet(selectedCategories);\n\n    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());\n\n    // Set the dialog title\n    builder.setTitle(\"News Feed Categories\")\n\n        // Specify the list array, the items to be selected by default (the EnumSet from DroidBoyActivity),\n        // and the listener through which to receive callbacks when items are selected\n        .setMultiChoiceItems(CATEGORIES, mCategoryIsChecked,\n            (dialog, which, isChecked) -> {\n              ListView lv = ((AlertDialog)getDialog()).getListView();\n              if (which == 0) {\n                // The \"All\" option is clicked, we should update all other options to be checked/unchecked.\n                for (int i = 0; i < Arrays.asList(CATEGORIES).size(); i++) {\n                  lv.setItemChecked(i, isChecked);\n                  mCategoryIsChecked[i] = isChecked;\n                }\n              } else if (which < Arrays.asList(CATEGORIES).size()) {\n                mCategoryIsChecked[which] = isChecked;\n                if (!isChecked) {\n                  // When there is an option is unchecked, we should also unchecked the \"All\" option\n                  lv.setItemChecked(0, false);\n                  mCategoryIsChecked[0] = false;\n                }\n              }\n            }\n        )\n        // Set the action buttons\n        .setPositiveButton(\"OK\", (dialog, id) -> {\n          selectedCategories = getEnumSetFromBooleans(mCategoryIsChecked);\n          mListener.onDialogPositiveClick(this);\n        })\n        .setNegativeButton(\"Cancel\", (dialog, id) -> {\n        });\n    return builder.create();\n  }\n\n  @SuppressWarnings(\"checkstyle:localvariablename\")\n  private boolean[] getBooleansFromEnumSet(EnumSet<CardCategory> categories) {\n    boolean[] array = new boolean[CATEGORIES.length];\n    if (categories.equals(CardCategory.getAllCategories())) {\n      Arrays.fill(array, true);\n      return array;\n    } else {\n      Arrays.fill(array, false);\n      List<String> categoriesStringList = Arrays.asList(CATEGORIES);\n      for (int i = 0; i < categoriesStringList.size(); i ++) {\n        String category = categoriesStringList.get(i);\n        categoriesStringList.set(i, category.toUpperCase(Locale.US));\n      }\n      for (CardCategory category: categories) {\n        int i = categoriesStringList.indexOf(category.toString());\n        if (i >= 0) {\n          array[i] = true;\n        } else {\n          Log.i(TAG, \"Cannot find Category for\" + category.toString() + \"in the categories list.\");\n        }\n      }\n      return array;\n    }\n  }\n\n  private EnumSet<CardCategory> getEnumSetFromBooleans(boolean[] isChecked) {\n    EnumSet<CardCategory> set = EnumSet.noneOf(CardCategory.class);\n    if (isChecked[0]) {\n      set = CardCategory.getAllCategories();\n    } else {\n      for (int i = 1; i < Arrays.asList(CATEGORIES).size(); i ++) {\n        if (isChecked[i]) {\n          set.add(CardCategory.get(CATEGORIES[i]));\n        }\n      }\n      if (set.isEmpty()) {\n        set.add(CardCategory.NO_CATEGORY);\n      }\n    }\n    return set;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/FullyCustomNotificationFactory.kt",
    "content": "package com.appboy.sample\n\nimport android.app.Notification\nimport androidx.core.app.NotificationCompat\nimport com.braze.models.push.BrazeNotificationPayload\nimport com.braze.IBrazeNotificationFactory\nimport com.braze.push.BrazeNotificationUtils.getOrCreateNotificationChannelId\nimport com.braze.push.BrazeNotificationUtils.setAccentColorIfPresentAndSupported\n\nclass FullyCustomNotificationFactory : IBrazeNotificationFactory {\n    override fun createNotification(payload: BrazeNotificationPayload): Notification? {\n        val context = payload.context ?: return null\n\n        val notificationChannelId = getOrCreateNotificationChannelId(payload)\n        val notificationBuilder = NotificationCompat.Builder(context, notificationChannelId)\n        notificationBuilder.setContentTitle(payload.titleText)\n        notificationBuilder.setSmallIcon(R.drawable.com_braze_push_small_notification_icon)\n        setAccentColorIfPresentAndSupported(notificationBuilder, payload)\n        val contentString = parseContentsFromExtras(payload.extras)\n        notificationBuilder.setContentText(contentString)\n        return notificationBuilder.build()\n    }\n\n    private fun parseContentsFromExtras(extras: Map<String, String>): String = \"\"\"\n        Your order: ${extras[PushTesterFragment.EXAMPLE_APPBOY_EXTRA_KEY_1]}, \n        ${extras[PushTesterFragment.EXAMPLE_APPBOY_EXTRA_KEY_2]}, \n        ${extras[PushTesterFragment.EXAMPLE_APPBOY_EXTRA_KEY_3]}.\n    \"\"\".trimIndent()\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/InAppMessageTesterFragment.kt",
    "content": "package com.appboy.sample\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.AdapterView\nimport android.widget.Button\nimport android.widget.CheckBox\nimport android.widget.CompoundButton\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\nimport com.appboy.sample.util.SpinnerUtils\nimport com.braze.Braze\nimport com.braze.enums.inappmessage.ClickAction\nimport com.braze.enums.inappmessage.CropType\nimport com.braze.enums.inappmessage.DismissType\nimport com.braze.enums.inappmessage.ImageStyle\nimport com.braze.enums.inappmessage.MessageType\nimport com.braze.enums.inappmessage.Orientation\nimport com.braze.enums.inappmessage.SlideFrom\nimport com.braze.enums.inappmessage.TextAlign\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.IInAppMessageHtml\nimport com.braze.models.inappmessage.IInAppMessageImmersive\nimport com.braze.models.inappmessage.IInAppMessageWithImage\nimport com.braze.models.inappmessage.IInAppMessageZippedAssetHtml\nimport com.braze.models.inappmessage.InAppMessageFull\nimport com.braze.models.inappmessage.InAppMessageHtml\nimport com.braze.models.inappmessage.InAppMessageHtmlFull\nimport com.braze.models.inappmessage.InAppMessageModal\nimport com.braze.models.inappmessage.InAppMessageSlideup\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.BrazeDeeplinkHandler.Companion.setBrazeDeeplinkHandler\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams.graphicModalMaxHeightDp\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams.graphicModalMaxWidthDp\nimport com.braze.ui.inappmessage.config.BrazeInAppMessageParams.modalizedImageRadiusDp\nimport java.util.*\n\n@Suppress(\"LargeClass\")\nclass InAppMessageTesterFragment : Fragment(), AdapterView.OnItemSelectedListener {\n    private enum class HtmlMessageType(val fileName: String, val zippedAssetUrl: String?) {\n        NO_JS(\n            \"html_inapp_message_body_no_js.html\",\n            \"https://appboy-staging-dashboard-uploads.s3.amazonaws.com/zip_uploads/files/\" +\n                \"585c1776bf5cea3cbe1b36b2/124fae83d6ba4023d4ede28e9177980e6373747c/original.zip?1482430326\"\n        ),\n        INLINE_JS(\"html_inapp_message_body_inline_js.html\", null), EXTERNAL_JS(\n            \"html_inapp_message_body_external_js.html\",\n            \"https://appboy-staging-dashboard-uploads.s3.amazonaws.com/zip_uploads/files/\" +\n                \"585c18c3bf5cea3c861b36ba/b0c7e536230b34ef800c8e0ef0747eaac53545a5/original.zip?1482430659\"\n        ),\n        STAR_WARS(\n            \"html_inapp_message_body_star_wars.html\",\n            null\n        ),\n        YOUTUBE(\"html_inapp_message_body_youtube_iframe.html\", null), BRIDGE_TESTER(\n            \"html_in_app_message_bridge_tester.html\",\n            \"https://appboy-images.com/HTML_ZIP_STOPWATCH.zip\"\n        ),\n        SLOW_LOADING(\n            \"html_inapp_message_delayed_open.html\",\n            null\n        ),\n        DARK_MODE(\n            \"html_inapp_message_dark_mode.html\",\n            null\n        ),\n        UNIFIED_HTML_BOOTSTRAP_ALBUM(\n            \"html_in_app_message_unified_bootstrap_album.html\",\n            null\n        ),\n        SHARK_HTML(\"html_shark_unified.html\", null);\n    }\n\n    private var messageType: String? = null\n    private var clickAction: String? = null\n    private var dismissType: String? = null\n    private var slideFrom: String? = null\n    private var uri: String? = null\n    private var header: String? = null\n    private var message: String? = null\n    private var backgroundColor: String? = null\n    private var iconColor: String? = null\n    private var iconBackgroundColor: String? = null\n    private var closeButtonColor: String? = null\n    private var buttonBorderColor: String? = null\n    private var textColor: String? = null\n    private var headerTextColor: String? = null\n    private var buttonColor: String? = null\n    private var buttonTextColor: String? = null\n    private var frameColor: String? = null\n    private var icon: String? = null\n    private var image: String? = null\n    private var buttons: String? = null\n    private var orientation: String? = null\n    private var messageTextAlign: String? = null\n    private var headerTextAlign: String? = null\n    private var animateIn: String? = null\n    private var animateOut: String? = null\n    private var useInWebView: String? = null\n\n    private val settingsPreferences: SharedPreferences\n        get() = requireActivity().getPreferences(Context.MODE_PRIVATE)\n\n    @Suppress(\"ComplexMethod\")\n    override fun onCreateView(\n        layoutInflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val view = layoutInflater.inflate(R.layout.inappmessage_tester, container, false)\n        for (key in spinnerOptionMap.keys) {\n            spinnerOptionMap[key]?.let {\n                SpinnerUtils.setUpSpinner(\n                    view.findViewById(key), this,\n                    it\n                )\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_inappmessage_view_factory_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance()\n                    .setCustomInAppMessageViewFactory(CustomInAppMessageViewFactory())\n            } else {\n                BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewFactory(null)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_inappmessage_manager_listener_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance().setCustomInAppMessageManagerListener(\n                    CustomInAppMessageManagerListener(\n                        activity\n                    )\n                )\n            } else {\n                BrazeInAppMessageManager.getInstance().setCustomInAppMessageManagerListener(null)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_appboy_navigator_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                setBrazeDeeplinkHandler(CustomBrazeDeeplinkHandler())\n            } else {\n                setBrazeDeeplinkHandler(null)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_appboy_graphic_modal_max_size_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                graphicModalMaxHeightDp = 420.0\n                graphicModalMaxWidthDp = 320.0\n            } else {\n                graphicModalMaxHeightDp = BrazeInAppMessageParams.GRAPHIC_MODAL_MAX_HEIGHT_DP\n                graphicModalMaxWidthDp = BrazeInAppMessageParams.GRAPHIC_MODAL_MAX_WIDTH_DP\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_appboy_image_radius_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            modalizedImageRadiusDp = if (isChecked) {\n                0.0\n            } else {\n                BrazeInAppMessageParams.MODALIZED_IMAGE_RADIUS_DP\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.disable_back_button_dismiss_behavior)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance().setBackButtonDismissesInAppMessageView(false)\n            } else {\n                BrazeInAppMessageManager.getInstance().setBackButtonDismissesInAppMessageView(true)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.enable_tap_outside_modal_dismiss_behavior)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance().setClickOutsideModalViewDismissInAppMessageView(true)\n            } else {\n                BrazeInAppMessageManager.getInstance()\n                    .setClickOutsideModalViewDismissInAppMessageView(false)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_appboy_animation_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance()\n                    .setCustomInAppMessageAnimationFactory(CustomInAppMessageAnimationFactory())\n            } else {\n                BrazeInAppMessageManager.getInstance().setCustomInAppMessageAnimationFactory(null)\n            }\n        }\n        setupCheckbox(\n            view.findViewById(R.id.custom_appboy_html_inappmessage_action_listener_checkbox)\n        ) { _: CompoundButton, isChecked: Boolean ->\n            if (isChecked) {\n                BrazeInAppMessageManager.getInstance().setCustomHtmlInAppMessageActionListener(\n                    CustomHtmlInAppMessageActionListener(\n                        context\n                    )\n                )\n            } else {\n                BrazeInAppMessageManager.getInstance().setCustomHtmlInAppMessageActionListener(null)\n            }\n        }\n        val createAndAddInAppMessageButton =\n            view.findViewById<Button>(R.id.create_and_add_inappmessage_button)\n        createAndAddInAppMessageButton.setOnClickListener {\n            if (settingsPreferences.getBoolean(\n                    CUSTOM_INAPPMESSAGE_VIEW_KEY, false\n                )\n            ) {\n                addInAppMessage(CustomInAppMessage())\n            } else {\n                when (messageType) {\n                    \"modal\" -> addInAppMessage(InAppMessageModal())\n                    \"modal_graphic\" -> {\n                        val inAppMessageModal = InAppMessageModal()\n                        inAppMessageModal.imageStyle = ImageStyle.GRAPHIC\n                        // graphic modals must be center cropped, the default for newly constructed modals\n                        // is center_fit\n                        inAppMessageModal.cropType = CropType.CENTER_CROP\n                        addInAppMessage(inAppMessageModal)\n                    }\n                    \"full\" -> addInAppMessage(InAppMessageFull())\n                    \"full_graphic\" -> {\n                        val inAppMessageFull = InAppMessageFull()\n                        inAppMessageFull.imageStyle = ImageStyle.GRAPHIC\n                        addInAppMessage(inAppMessageFull)\n                    }\n                    \"html_full_no_js\" -> addInAppMessage(InAppMessageHtmlFull(), HtmlMessageType.NO_JS)\n                    \"html_full_inline_js\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.INLINE_JS\n                    )\n                    \"html_full_external_js\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.EXTERNAL_JS\n                    )\n                    \"html_full_star_wars\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.STAR_WARS\n                    )\n                    \"html_full_youtube\" -> addInAppMessage(InAppMessageHtmlFull(), HtmlMessageType.YOUTUBE)\n                    \"html_full_bridge_tester\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.BRIDGE_TESTER\n                    )\n                    \"html_full_slow_loading\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.SLOW_LOADING\n                    )\n                    \"html_full_unified_bootstrap\" -> addInAppMessage(\n                        InAppMessageHtml(),\n                        HtmlMessageType.UNIFIED_HTML_BOOTSTRAP_ALBUM\n                    )\n                    \"html_shark_unified\" -> addInAppMessage(InAppMessageHtml(), HtmlMessageType.SHARK_HTML)\n                    \"html_full_dark_mode\" -> addInAppMessage(\n                        InAppMessageHtmlFull(),\n                        HtmlMessageType.DARK_MODE\n                    )\n                    \"modal_dark_theme\" -> {\n                        val darkModeJson = context?.let { context ->\n                            getStringFromAssets(\n                                context, \"modal_inapp_message_with_dark_theme.json\"\n                            )\n                        }\n                        darkModeJson?.let { json -> addInAppMessageFromString(json) }\n                    }\n                    \"slideup\" -> addInAppMessage(InAppMessageSlideup())\n                    else -> addInAppMessage(InAppMessageSlideup())\n                }\n            }\n        }\n        val displayNextInAppMessageButton =\n            view.findViewById<Button>(R.id.display_next_inappmessage_button)\n        displayNextInAppMessageButton.setOnClickListener {\n            BrazeInAppMessageManager.getInstance().requestDisplayInAppMessage()\n        }\n        val hideCurrentInAppMessageButton =\n            view.findViewById<Button>(R.id.hide_current_inappmessage_button)\n        hideCurrentInAppMessageButton.setOnClickListener {\n            BrazeInAppMessageManager.getInstance().hideCurrentlyDisplayingInAppMessage(false)\n        }\n        return view\n    }\n\n    private fun addInAppMessageImmersive(inAppMessage: IInAppMessageImmersive) {\n        if (inAppMessage is InAppMessageModal) {\n            inAppMessage.message = \"Welcome to Braze! Braze is Marketing Automation for Apps!\"\n            if (inAppMessage.imageStyle == ImageStyle.GRAPHIC) {\n                inAppMessage.remoteImageUrl =\n                    resources.getString(R.string.appboy_image_url_1000w_1000h)\n            } else {\n                inAppMessage.remoteImageUrl =\n                    resources.getString(R.string.appboy_image_url_1160w_400h)\n            }\n        } else if (inAppMessage is InAppMessageFull) {\n            inAppMessage.message =\n                \"Welcome to Braze! Braze is Marketing Automation for Apps. This is an example of a full in-app message.\"\n            if (inAppMessage.imageStyle == ImageStyle.GRAPHIC) {\n                if (inAppMessage.orientation == Orientation.LANDSCAPE) {\n                    inAppMessage.remoteImageUrl =\n                        resources.getString(R.string.appboy_image_url_1600w_1000h)\n                } else {\n                    inAppMessage.remoteImageUrl =\n                        resources.getString(R.string.appboy_image_url_1000w_1600h)\n                }\n            } else {\n                if (inAppMessage.orientation == Orientation.LANDSCAPE) {\n                    inAppMessage.remoteImageUrl =\n                        resources.getString(R.string.appboy_image_url_1600w_500h)\n                } else {\n                    inAppMessage.remoteImageUrl =\n                        resources.getString(R.string.appboy_image_url_1000w_800h)\n                }\n            }\n        }\n        inAppMessage.header = \"Hello from Braze!\"\n        val messageButtons = ArrayList<MessageButton>()\n        val buttonOne = MessageButton()\n        buttonOne.text = \"NewsFeed\"\n        buttonOne.setClickBehavior(ClickAction.NEWS_FEED)\n        messageButtons.add(buttonOne)\n        inAppMessage.messageButtons = messageButtons\n        addMessageButtons(inAppMessage)\n        setHeader(inAppMessage)\n        setCloseButtonColor(inAppMessage)\n        setFrameColor(inAppMessage)\n        setHeaderTextAlign(inAppMessage)\n    }\n\n    /**\n     * Adds an [IInAppMessage] from its [IInAppMessage.forJsonPut] form.\n     */\n    private fun addInAppMessageFromString(serializedInAppMessage: String) {\n        val inAppMessage =\n            Braze.getInstance(requireContext()).deserializeInAppMessageString(serializedInAppMessage)\n        BrazeInAppMessageManager.getInstance().addInAppMessage(inAppMessage)\n    }\n\n    private fun addInAppMessageSlideup(inAppMessage: InAppMessageSlideup) {\n        inAppMessage.message = \"Welcome to Braze! This is a slideup in-app message.\"\n        inAppMessage.icon = \"\\uf091\"\n        inAppMessage.setClickBehavior(ClickAction.NEWS_FEED)\n        setSlideFrom(inAppMessage)\n        setChevronColor(inAppMessage)\n    }\n\n    private fun addInAppMessageCustom(inAppMessage: IInAppMessage) {\n        inAppMessage.message = \"Welcome to Braze! This is a custom in-app message.\"\n        inAppMessage.icon = \"\\uf091\"\n    }\n\n    private fun addInAppMessageHtml(\n        inAppMessage: IInAppMessageHtml,\n        htmlMessageType: HtmlMessageType\n    ) {\n        inAppMessage.message = context?.let { getStringFromAssets(it, htmlMessageType.fileName) }\n        if (htmlMessageType.zippedAssetUrl != null && inAppMessage is IInAppMessageZippedAssetHtml) {\n            inAppMessage.assetsZipRemoteUrl = htmlMessageType.zippedAssetUrl\n        }\n    }\n\n    private fun addInAppMessage(inAppMessage: IInAppMessage, messageType: HtmlMessageType? = null) {\n        // set orientation early to help determine which default image to use\n        setOrientation(inAppMessage)\n        when (inAppMessage.messageType) {\n            MessageType.SLIDEUP -> addInAppMessageSlideup(inAppMessage as InAppMessageSlideup)\n            MessageType.MODAL, MessageType.FULL -> addInAppMessageImmersive(inAppMessage as IInAppMessageImmersive)\n            MessageType.HTML, MessageType.HTML_FULL -> messageType?.let {\n                addInAppMessageHtml(\n                    inAppMessage as IInAppMessageHtml,\n                    it\n                )\n            }\n            else -> addInAppMessageCustom(inAppMessage)\n        }\n        if (!addClickAction(inAppMessage)) {\n            return\n        }\n        setDismissType(inAppMessage)\n        setBackgroundColor(inAppMessage)\n        setMessage(inAppMessage)\n        setIcon(inAppMessage)\n        if (inAppMessage is IInAppMessageWithImage) {\n            setImage(inAppMessage as IInAppMessageWithImage)\n        }\n        setMessageTextAlign(inAppMessage)\n        setAnimation(inAppMessage)\n        setUseWebview(inAppMessage)\n        BrazeInAppMessageManager.getInstance().addInAppMessage(inAppMessage)\n    }\n\n    private fun setUseWebview(inAppMessage: IInAppMessage) {\n        if (!SpinnerUtils.spinnerItemNotSet(useInWebView)) {\n            if (useInWebView == \"true\") {\n                inAppMessage.openUriInWebView = true\n            } else if (useInWebView == \"false\") {\n                inAppMessage.openUriInWebView = false\n            }\n        }\n    }\n\n    private fun setAnimation(inAppMessage: IInAppMessage) {\n        if (!SpinnerUtils.spinnerItemNotSet(animateIn)) {\n            if (animateIn == \"true\") {\n                inAppMessage.animateIn = true\n            } else if (animateIn == \"false\") {\n                inAppMessage.animateIn = false\n            }\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(animateOut)) {\n            if (animateOut == \"true\") {\n                inAppMessage.animateOut = true\n            } else if (animateOut == \"false\") {\n                inAppMessage.animateOut = false\n            }\n        }\n    }\n\n    private fun setDismissType(inAppMessage: IInAppMessage) {\n        // set dismiss type if defined\n        when (dismissType) {\n            \"auto\" -> {\n                inAppMessage.dismissType = DismissType.AUTO_DISMISS\n            }\n            \"auto-short\" -> {\n                inAppMessage.dismissType = DismissType.AUTO_DISMISS\n                inAppMessage.durationInMilliseconds = 1000\n            }\n            \"manual\" -> {\n                inAppMessage.dismissType = DismissType.MANUAL\n            }\n            else -> {\n                inAppMessage.dismissType = DismissType.MANUAL\n            }\n        }\n    }\n\n    private fun setBackgroundColor(inAppMessage: IInAppMessage) {\n        // set background color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(backgroundColor)) {\n            backgroundColor?.let { inAppMessage.backgroundColor = parseColorFromString(it) }\n        }\n    }\n\n    private fun setChevronColor(inAppMessage: InAppMessageSlideup) {\n        // set chevron color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(closeButtonColor)) {\n            closeButtonColor?.let { inAppMessage.chevronColor = parseColorFromString(it) }\n        }\n    }\n\n    private fun setCloseButtonColor(inAppMessage: IInAppMessageImmersive) {\n        // set close button color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(closeButtonColor)) {\n            closeButtonColor?.let { inAppMessage.closeButtonColor = parseColorFromString(it) }\n        }\n    }\n\n    private fun setMessage(inAppMessage: IInAppMessage) {\n        // set text color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(textColor)) {\n            textColor?.let { inAppMessage.messageTextColor = parseColorFromString(it) }\n        }\n        // don't replace message on html in-app messages\n        if (inAppMessage is IInAppMessageHtml) {\n            return\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(message)) {\n            inAppMessage.message = message\n        }\n    }\n\n    private fun setIcon(inAppMessage: IInAppMessage) {\n        // set icon color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(iconColor)) {\n            iconColor?.let { inAppMessage.iconColor = parseColorFromString(it) }\n        }\n        // set icon background color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(iconBackgroundColor)) {\n            iconBackgroundColor?.let { inAppMessage.iconBackgroundColor = parseColorFromString(it) }\n        }\n        // set in-app message icon\n        if (!SpinnerUtils.spinnerItemNotSet(icon)) {\n            if (icon == getString(R.string.none)) {\n                inAppMessage.icon = null\n            } else {\n                inAppMessage.icon = icon\n            }\n        }\n    }\n\n    private fun setImage(inAppMessage: IInAppMessageWithImage) {\n        // set in-app message image url\n        if (!SpinnerUtils.spinnerItemNotSet(image)) {\n            if (image == getString(R.string.none)) {\n                inAppMessage.remoteImageUrl = null\n            } else {\n                inAppMessage.remoteImageUrl = image\n            }\n        }\n    }\n\n    private fun setOrientation(inAppMessage: IInAppMessage) {\n        // set in-app message preferred orientation\n        if (!SpinnerUtils.spinnerItemNotSet(orientation)) {\n            when (orientation) {\n                \"any\" -> inAppMessage.orientation = Orientation.ANY\n                \"portrait\" -> inAppMessage.orientation = Orientation.PORTRAIT\n                \"landscape\" -> inAppMessage.orientation = Orientation.LANDSCAPE\n                else -> {}\n            }\n        }\n    }\n\n    private fun addClickAction(inAppMessage: IInAppMessage): Boolean {\n        // set click action if defined\n        if (\"newsfeed\" == clickAction) {\n            inAppMessage.setClickBehavior(ClickAction.NEWS_FEED)\n        } else if (\"uri\" == clickAction) {\n            if (SpinnerUtils.spinnerItemNotSet(uri)) {\n                Toast.makeText(context, \"Please choose a URI.\", Toast.LENGTH_LONG).show()\n                return false\n            } else {\n                inAppMessage.setClickBehavior(ClickAction.URI, Uri.parse(uri))\n            }\n        } else if (getString(R.string.none) == clickAction) {\n            inAppMessage.setClickBehavior(ClickAction.NONE)\n        }\n        return true\n    }\n\n    private fun setSlideFrom(inAppMessage: InAppMessageSlideup) {\n        // set slide from if defined\n        if (\"top\" == slideFrom) {\n            inAppMessage.slideFrom = SlideFrom.TOP\n        } else if (\"bottom\" == slideFrom) {\n            inAppMessage.slideFrom = SlideFrom.BOTTOM\n        }\n    }\n\n    private fun setHeader(inAppMessage: IInAppMessageImmersive) {\n        // set header text color if defined\n        if (!SpinnerUtils.spinnerItemNotSet(headerTextColor)) {\n            headerTextColor?.let { inAppMessage.headerTextColor = parseColorFromString(it) }\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(header)) {\n            if (getString(R.string.none) == header) {\n                inAppMessage.header = null\n            } else {\n                inAppMessage.header = header\n            }\n        }\n    }\n\n    private fun setFrameColor(inAppMessage: IInAppMessageImmersive) {\n        if (!SpinnerUtils.spinnerItemNotSet(frameColor)) {\n            inAppMessage.frameColor = frameColor?.let { parseColorFromString(it) }\n        }\n    }\n\n    private fun setHeaderTextAlign(inAppMessage: IInAppMessageImmersive) {\n        if (!SpinnerUtils.spinnerItemNotSet(headerTextAlign)) {\n            headerTextAlign?.let { inAppMessage.headerTextAlign = parseTextAlign(it) }\n        }\n    }\n\n    private fun setMessageTextAlign(inAppMessage: IInAppMessage) {\n        if (!SpinnerUtils.spinnerItemNotSet(messageTextAlign)) {\n            messageTextAlign?.let { inAppMessage.messageTextAlign = parseTextAlign(it) }\n        }\n    }\n\n    private fun parseTextAlign(textAlign: String): TextAlign {\n        return when (textAlign) {\n            \"start\" -> TextAlign.START\n            \"end\" -> TextAlign.END\n            \"center\" -> TextAlign.CENTER\n            else -> TextAlign.START\n        }\n    }\n\n    @Suppress(\"ComplexMethod\")\n    private fun addMessageButtons(inAppMessage: IInAppMessageImmersive) {\n        if (!SpinnerUtils.spinnerItemNotSet(buttons)) {\n            if (getString(R.string.none) == buttons) {\n                inAppMessage.messageButtons = emptyList()\n                return\n            }\n            val messageButtons = ArrayList<MessageButton>()\n            val buttonOne = MessageButton()\n            val buttonTwo = MessageButton()\n            when (buttons) {\n                \"one\" -> {\n                    buttonOne.setClickBehavior(ClickAction.NEWS_FEED)\n                    buttonOne.text = \"News Feed\"\n                    messageButtons.add(buttonOne)\n                }\n                \"one_long\" -> {\n                    buttonOne.setClickBehavior(ClickAction.NEWS_FEED)\n                    buttonOne.text = getString(R.string.message_2400)\n                    messageButtons.add(buttonOne)\n                }\n                \"push_prompt_one\" -> {\n                    val pushPromptBrazeActionUri = Uri.parse(getStringFromAssets(requireContext(), \"braze_actions/show_push_prompt.txt\"))\n                    buttonOne.setClickBehavior(ClickAction.URI, pushPromptBrazeActionUri)\n                    buttonOne.text = \"Show Push Prompt\"\n                    messageButtons.add(buttonOne)\n                }\n            }\n            when (buttons) {\n                \"two\", \"long\" -> {\n                    buttonOne.text = \"No Webview\"\n                    buttonOne.setClickBehavior(\n                        ClickAction.URI,\n                        Uri.parse(resources.getString(R.string.braze_homepage_url))\n                    )\n                    buttonTwo.text = \"Webview\"\n                    buttonTwo.setClickBehavior(\n                        ClickAction.URI,\n                        Uri.parse(resources.getString(R.string.braze_homepage_url))\n                    )\n                    buttonTwo.openUriInWebview = true\n                    if (\"long\" == buttons) {\n                        buttonOne.text = \"No Webview WITH A VERY LONG TITLE\"\n                        buttonTwo.text = \"Webview WITH A VERY LONG TITLE\"\n                    }\n                    messageButtons.add(buttonOne)\n                    messageButtons.add(buttonTwo)\n                }\n                \"deeplink\" -> {\n                    buttonOne.text = \"TELEPHONE\"\n                    buttonOne.setClickBehavior(\n                        ClickAction.URI,\n                        Uri.parse(resources.getString(R.string.telephone_uri))\n                    )\n                    buttonTwo.text = \"PLAY STORE\"\n                    buttonTwo.setClickBehavior(\n                        ClickAction.URI,\n                        Uri.parse(resources.getString(R.string.play_store_uri))\n                    )\n                    messageButtons.add(buttonOne)\n                    messageButtons.add(buttonTwo)\n                }\n            }\n            inAppMessage.messageButtons = messageButtons\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(buttonColor) && inAppMessage.messageButtons.isNotEmpty()) {\n            for (button in inAppMessage.messageButtons) {\n                buttonColor?.let { button.backgroundColor = parseColorFromString(it) }\n            }\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(buttonTextColor) && inAppMessage.messageButtons.isNotEmpty()) {\n            for (button in inAppMessage.messageButtons) {\n                buttonTextColor?.let { button.textColor = parseColorFromString(it) }\n            }\n        }\n        if (!SpinnerUtils.spinnerItemNotSet(buttonBorderColor) && inAppMessage.messageButtons.isNotEmpty()) {\n            for (button in inAppMessage.messageButtons) {\n                buttonBorderColor?.let { button.borderColor = parseColorFromString(it) }\n            }\n        }\n    }\n\n    @Suppress(\"ComplexMethod\")\n    override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {\n        when (parent.id) {\n            R.id.inapp_set_message_type_spinner ->\n                messageType =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_message_type_values)\n            R.id.inapp_click_action_spinner ->\n                clickAction =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_click_action_values)\n            R.id.inapp_dismiss_type_spinner ->\n                dismissType =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_dismiss_type_values)\n            R.id.inapp_slide_from_spinner ->\n                slideFrom =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_slide_from_values)\n            R.id.inapp_uri_spinner ->\n                uri =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_uri_values)\n            R.id.inapp_header_spinner ->\n                header =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_header_values)\n            R.id.inapp_message_spinner ->\n                message =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_message_values)\n            R.id.inapp_background_color_spinner ->\n                backgroundColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_icon_color_spinner ->\n                iconColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_icon_background_color_spinner ->\n                iconBackgroundColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_close_button_color_spinner ->\n                closeButtonColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_text_color_spinner ->\n                textColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_header_text_color_spinner ->\n                headerTextColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_button_color_spinner ->\n                buttonColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_button_border_color_spinner ->\n                buttonBorderColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_button_text_color_spinner ->\n                buttonTextColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_color_values)\n            R.id.inapp_frame_spinner ->\n                frameColor =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_frame_values)\n            R.id.inapp_icon_spinner ->\n                icon =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_icon_values)\n            R.id.inapp_image_spinner ->\n                image =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_image_values)\n            R.id.inapp_button_spinner ->\n                buttons =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_button_values)\n            R.id.inapp_orientation_spinner ->\n                orientation =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_orientation_values)\n            R.id.inapp_header_align_spinner ->\n                headerTextAlign =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_align_values)\n            R.id.inapp_message_align_spinner ->\n                messageTextAlign =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_align_values)\n            R.id.inapp_animate_in_spinner ->\n                animateIn =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_boolean_values)\n            R.id.inapp_animate_out_spinner ->\n                animateOut =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_boolean_values)\n            R.id.inapp_open_uri_in_webview_spinner ->\n                useInWebView =\n                    SpinnerUtils.handleSpinnerItemSelected(parent, R.array.inapp_boolean_values)\n            else -> brazelog(E) { \"Item selected for unknown spinner\" }\n        }\n    }\n\n    override fun onNothingSelected(parent: AdapterView<*>?) {\n        // Do nothing\n    }\n\n    private fun parseColorFromString(colorString: String): Int {\n        return when (colorString) {\n            \"red\" -> BRAZE_RED\n            \"orange\" -> GOOGLE_ORANGE\n            \"yellow\" -> GOOGLE_YELLOW\n            \"green\" -> GOOGLE_GREEN\n            \"blue\" -> BRAZE_BLUE\n            \"purple\" -> GOOGLE_PURPLE\n            \"brown\" -> GOOGLE_BROWN\n            \"grey\" -> GOOGLE_GREY\n            \"black\" -> BLACK\n            \"white\" -> WHITE\n            \"transparent\" -> Color.argb(0, 0, 0, 0)\n            \"almost_transparent_blue\" -> TRANSPARENT_BRAZE_BLUE\n            else -> 0\n        }\n    }\n\n    private fun setupCheckbox(\n        checkBoxView: CheckBox,\n        listener: CompoundButton.OnCheckedChangeListener\n    ) {\n        // Generate the preferences id. Note that this will change\n        // if the id changes but that is ok for this use-case\n        val key = \"checkbox_pref_\" + checkBoxView.id\n\n        // Set the initial checked state\n        checkBoxView.isChecked = settingsPreferences.getBoolean(key, false)\n\n        // Call the provided listener\n        checkBoxView.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            listener.onCheckedChanged(buttonView, isChecked)\n            settingsPreferences\n                .edit()\n                .putBoolean(key, isChecked)\n                .apply()\n        }\n    }\n\n    companion object {\n        private const val CUSTOM_INAPPMESSAGE_VIEW_KEY = \"inapmessages_custom_inappmessage_view\"\n\n        private const val BRAZE_RED = -0xcc1c2\n        private const val GOOGLE_ORANGE = -0xa8de\n        private const val GOOGLE_YELLOW = -0x14c5\n        private const val GOOGLE_GREEN = -0xb350b0\n        private const val BRAZE_BLUE = -0xff8c2b\n        private const val TRANSPARENT_BRAZE_BLUE = 0x220073d5\n        private const val GOOGLE_PURPLE = -0x98c549\n        private const val GOOGLE_BROWN = -0x86aab8\n        private const val GOOGLE_GREY = -0x616162\n        private const val BLACK = -0x1000000\n        private const val WHITE = -0x1\n        private var spinnerOptionMap: Map<Int, Int>\n\n        init {\n            val spinnerMap: HashMap<Int, Int> = HashMap()\n            spinnerMap[R.id.inapp_set_message_type_spinner] = R.array.inapp_message_type_options\n            spinnerMap[R.id.inapp_click_action_spinner] = R.array.inapp_click_action_options\n            spinnerMap[R.id.inapp_dismiss_type_spinner] = R.array.inapp_dismiss_type_options\n            spinnerMap[R.id.inapp_slide_from_spinner] = R.array.inapp_slide_from_options\n            spinnerMap[R.id.inapp_header_spinner] = R.array.inapp_header_options\n            spinnerMap[R.id.inapp_message_spinner] = R.array.inapp_message_options\n            spinnerMap[R.id.inapp_background_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_icon_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_icon_background_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_close_button_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_text_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_header_text_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_button_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_button_border_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_button_text_color_spinner] = R.array.inapp_color_options\n            spinnerMap[R.id.inapp_frame_spinner] = R.array.inapp_frame_options\n            spinnerMap[R.id.inapp_uri_spinner] = R.array.inapp_uri_options\n            spinnerMap[R.id.inapp_icon_spinner] = R.array.inapp_icon_options\n            spinnerMap[R.id.inapp_image_spinner] = R.array.inapp_image_options\n            spinnerMap[R.id.inapp_button_spinner] = R.array.inapp_button_options\n            spinnerMap[R.id.inapp_orientation_spinner] = R.array.inapp_orientation_options\n            spinnerMap[R.id.inapp_header_align_spinner] = R.array.inapp_align_options\n            spinnerMap[R.id.inapp_message_align_spinner] = R.array.inapp_align_options\n            spinnerMap[R.id.inapp_animate_in_spinner] = R.array.inapp_boolean_options\n            spinnerMap[R.id.inapp_animate_out_spinner] = R.array.inapp_boolean_options\n            spinnerMap[R.id.inapp_open_uri_in_webview_spinner] = R.array.inapp_boolean_options\n            spinnerOptionMap = Collections.unmodifiableMap(spinnerMap)\n        }\n\n        private fun getStringFromAssets(context: Context, filename: String): String =\n            context.assets.open(filename).bufferedReader().use {\n                it.readText()\n            }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/MainFragment.kt",
    "content": "package com.appboy.sample\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ArrayAdapter\nimport android.widget.AutoCompleteTextView\nimport android.widget.Button\nimport android.widget.EditText\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.lifecycleScope\nimport com.braze.enums.Gender\nimport com.braze.enums.Month\nimport com.braze.enums.NotificationSubscriptionType\nimport com.braze.events.IValueCallback\nimport com.braze.models.outgoing.AttributionData\nimport com.braze.Braze\nimport com.braze.BrazeUser\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.convertStringJsonArrayToList\nimport com.google.android.gms.ads.identifier.AdvertisingIdClient\nimport com.google.firebase.crashlytics.FirebaseCrashlytics\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.json.JSONArray\nimport org.json.JSONException\nimport java.math.BigDecimal\nimport java.util.*\n\nclass MainFragment : Fragment() {\n    private lateinit var customEventTextView: AutoCompleteTextView\n    private lateinit var customPurchaseTextView: AutoCompleteTextView\n    private lateinit var sharedPreferences: SharedPreferences\n    private lateinit var aliasEditText: EditText\n    private lateinit var aliasLabelEditText: EditText\n    private lateinit var customEventsAndPurchasesArrayAdapter: ArrayAdapter<String?>\n    private val lastSeenCustomEventsAndPurchases: Queue<String?> = LinkedList()\n\n    override fun onCreateView(\n        layoutInflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        val contentView = layoutInflater.inflate(R.layout.main_fragment, container, false)\n        sharedPreferences = requireActivity().getSharedPreferences(\"droidboy\", Context.MODE_PRIVATE)\n        customEventTextView =\n            contentView.findViewById(R.id.com_appboy_sample_custom_event_autocomplete_text_view)\n        customPurchaseTextView =\n            contentView.findViewById(R.id.com_appboy_sample_purchase_autocomplete_text_view)\n        customEventsAndPurchasesArrayAdapter = ArrayAdapter(\n            requireContext(),\n            android.R.layout.simple_list_item_1,\n            getLastSeenCustomEventsAndPurchasesFromLocalStorage()\n        )\n        customEventTextView.setAdapter(customEventsAndPurchasesArrayAdapter)\n        customPurchaseTextView.setAdapter(customEventsAndPurchasesArrayAdapter)\n\n        val userIdEditText: EditText =\n            contentView.findViewById(R.id.com_appboy_sample_set_user_id_edit_text)\n        userIdEditText.setText(sharedPreferences.getString(USER_ID_KEY, null))\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_set_user_id_button) {\n            val userId = userIdEditText.text.toString()\n            if (userId.isNotBlank()) {\n                (requireActivity().applicationContext as DroidboyApplication).changeUserWithNewSdkAuthToken(userId)\n                Toast.makeText(requireContext(), \"Set userId to: $userId\", Toast.LENGTH_SHORT)\n                    .show()\n                val editor = sharedPreferences.edit()\n                editor.putString(USER_ID_KEY, userId)\n                editor.apply()\n                FirebaseCrashlytics.getInstance().setUserId(userId)\n            } else {\n                Toast.makeText(requireContext(), \"Please enter a userId.\", Toast.LENGTH_SHORT)\n                    .show()\n            }\n        }\n\n        contentView.setOnButtonClickWithEditableText(\n            R.id.com_appboy_sample_set_sdk_auth_signature_button,\n            R.id.com_appboy_sample_set_sdk_auth_signature_edit_text\n        ) { _, signature ->\n            if (signature.isNotBlank()) {\n                Braze.getInstance(requireContext()).setSdkAuthenticationSignature(signature)\n                Toast.makeText(requireContext(), \"Set signature to: $signature\", Toast.LENGTH_SHORT)\n                    .show()\n            } else {\n                Toast.makeText(requireContext(), \"Please enter a signature.\", Toast.LENGTH_SHORT)\n                    .show()\n            }\n        }\n\n        aliasEditText = contentView.findViewById(R.id.com_appboy_sample_set_alias_edit_text)\n        aliasLabelEditText =\n            contentView.findViewById(R.id.com_appboy_sample_set_alias_label_edit_text)\n        contentView.setOnButtonClick(R.id.com_appboy_sample_set_user_alias_button) { handleAliasClick() }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_log_custom_event_button) {\n            val customEvent = customEventTextView.text.toString()\n            if (customEvent.isNotBlank()) {\n                Braze.getInstance(requireContext()).logCustomEvent(customEvent)\n                Toast.makeText(\n                    requireContext(),\n                    String.format(\"Logged custom event %s.\", customEvent),\n                    Toast.LENGTH_SHORT\n                ).show()\n                onCustomEventOrPurchaseLogged(customEvent)\n            } else {\n                Toast.makeText(requireContext(), \"Please enter a custom event.\", Toast.LENGTH_SHORT)\n                    .show()\n            }\n        }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_log_purchase_button) {\n            val purchase = customPurchaseTextView.text.toString()\n            if (purchase.isNotBlank()) {\n                Braze.getInstance(requireContext()).logPurchase(purchase, \"USD\", BigDecimal.TEN)\n                Toast.makeText(\n                    requireContext(),\n                    String.format(\"Logged purchase %s.\", purchase),\n                    Toast.LENGTH_SHORT\n                ).show()\n                onCustomEventOrPurchaseLogged(purchase)\n            } else {\n                Toast.makeText(requireContext(), \"Please enter a purchase.\", Toast.LENGTH_SHORT)\n                    .show()\n            }\n        }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_set_user_attributes_button) {\n            Braze.getInstance(requireContext()).getCurrentUser(object : IValueCallback<BrazeUser> {\n                override fun onSuccess(value: BrazeUser) {\n                    value.setFirstName(\"first name least\")\n                    value.setLastName(\"lastName\")\n                    value.setEmail(\"email@test.com\")\n                    value.setGender(Gender.FEMALE)\n                    value.setCountry(\"USA\")\n                    value.setLanguage(\"cs\")\n                    value.setHomeCity(\"New York\")\n                    value.setPhoneNumber(\"1234567890\")\n                    value.setDateOfBirth(1984, Month.AUGUST, 18)\n                    value.setPushNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN)\n                    value.setEmailNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN)\n                    value.setCustomUserAttribute(STRING_ATTRIBUTE_KEY, \"stringValue\")\n                    value.setCustomUserAttribute(FLOAT_ATTRIBUTE_KEY, 1.5f)\n                    value.setCustomUserAttribute(INT_ATTRIBUTE_KEY, 100)\n                    value.setCustomUserAttribute(BOOL_ATTRIBUTE_KEY, true)\n                    value.setCustomUserAttribute(LONG_ATTRIBUTE_KEY, 10L)\n                    value.setCustomUserAttribute(INCREMENT_ATTRIBUTE_KEY, 1)\n                    value.setCustomUserAttribute(DOUBLE_ATTRIBUTE_KEY, 3.1)\n                    value.incrementCustomUserAttribute(INCREMENT_ATTRIBUTE_KEY, 4)\n                    value.setCustomUserAttributeToSecondsFromEpoch(\n                        DATE_ATTRIBUTE_KEY,\n                        Date().time / 1000L\n                    )\n                    value.setCustomAttributeArray(\n                        STRING_ARRAY_ATTRIBUTE_KEY,\n                        arrayOf(\"a\", \"b\")\n                    )\n                    value.addToCustomAttributeArray(ARRAY_ATTRIBUTE_KEY, \"c\")\n                    value.removeFromCustomAttributeArray(ARRAY_ATTRIBUTE_KEY, \"b\")\n                    value.addToCustomAttributeArray(PETS_ARRAY_ATTRIBUTE_KEY, \"cat\")\n                    value.addToCustomAttributeArray(PETS_ARRAY_ATTRIBUTE_KEY, \"dog\")\n                    value.removeFromCustomAttributeArray(PETS_ARRAY_ATTRIBUTE_KEY, \"bird\")\n                    value.removeFromCustomAttributeArray(PETS_ARRAY_ATTRIBUTE_KEY, \"deer\")\n                    value.setAttributionData(\n                        AttributionData(\n                            \"network\",\n                            \"campaign\",\n                            \"ad group\",\n                            \"creative\"\n                        )\n                    )\n                    value.setLocationCustomAttribute(\n                        \"Favorite Location\",\n                        33.078883,\n                        -116.603131\n                    )\n                    showToast(\"Set user attributes.\")\n                }\n\n                override fun onError() {\n                    showToast(\"Failed to set user attributes.\")\n                }\n            })\n        }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_unset_user_attributes_button) {\n            Braze.getInstance(requireContext()).getCurrentUser(object : IValueCallback<BrazeUser> {\n                override fun onSuccess(value: BrazeUser) {\n                    // Unset current user default attributes\n                    value.setFirstName(null)\n                    value.setLastName(null)\n                    value.setEmail(null)\n                    value.setGender(Gender.UNKNOWN)\n                    value.setCountry(null)\n                    value.setLanguage(null)\n                    value.setHomeCity(null)\n                    value.setPhoneNumber(null)\n                    value.setDateOfBirth(1970, Month.JANUARY, 1)\n                    value.setPushNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED)\n                    value.setEmailNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED)\n                    // Unset current user custom attributes\n                    value.unsetCustomUserAttribute(STRING_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(FLOAT_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(INT_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(BOOL_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(LONG_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(DATE_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(ARRAY_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(STRING_ARRAY_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(PETS_ARRAY_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(INCREMENT_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(DOUBLE_ATTRIBUTE_KEY)\n                    value.unsetCustomUserAttribute(ATTRIBUTION_DATA_KEY)\n                    value.unsetLocationCustomAttribute(\"Mediocre Location\")\n                    showToast(\"Unset user attributes.\")\n                }\n\n                override fun onError() {\n                    showToast(\"Failed to unset user attributes.\")\n                }\n            })\n        }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_request_flush_button) {\n            Braze.getInstance(requireContext()).requestImmediateDataFlush()\n            Toast.makeText(requireContext(), \"Requested data flush.\", Toast.LENGTH_SHORT).show()\n        }\n\n        contentView.setOnButtonClick(R.id.com_appboy_sample_collect_and_flush_google_advertising_id_button) {\n            this.lifecycleScope.launch(context = Dispatchers.IO) {\n                try {\n                    val advertisingIdInfo =\n                        AdvertisingIdClient.getAdvertisingIdInfo(requireContext())\n                    Braze.getInstance(requireContext()).setGoogleAdvertisingId(\n                        advertisingIdInfo.id,\n                        advertisingIdInfo.isLimitAdTrackingEnabled\n                    )\n                    Braze.getInstance(requireContext()).requestImmediateDataFlush()\n                } catch (e: Exception) {\n                    brazelog(E, e) { \"Failed to collect Google Advertising ID information.\" }\n                }\n            }\n        }\n        return contentView\n    }\n\n    private fun getLastSeenCustomEventsAndPurchasesFromLocalStorage(): Array<String?> {\n        val serializedEvents =\n            sharedPreferences.getString(LAST_SEEN_CUSTOM_EVENTS_AND_PURCHASES_PREFERENCE_KEY, null)\n        try {\n            if (serializedEvents != null) {\n                lastSeenCustomEventsAndPurchases.addAll(\n                    JSONArray(serializedEvents).convertStringJsonArrayToList()\n                )\n            }\n        } catch (e: JSONException) {\n            brazelog(E, e) { \"Failed to get recent events from storage\" }\n        }\n        return lastSeenCustomEventsAndPurchases.toTypedArray()\n    }\n\n    private fun onCustomEventOrPurchaseLogged(eventOrPurchaseName: String) {\n        if (lastSeenCustomEventsAndPurchases.contains(eventOrPurchaseName)) {\n            return\n        }\n        lastSeenCustomEventsAndPurchases.add(eventOrPurchaseName)\n        if (lastSeenCustomEventsAndPurchases.size > 5) {\n            lastSeenCustomEventsAndPurchases.remove()\n        }\n        val editor = sharedPreferences.edit()\n        editor.putString(\n            LAST_SEEN_CUSTOM_EVENTS_AND_PURCHASES_PREFERENCE_KEY,\n            JSONArray(lastSeenCustomEventsAndPurchases).toString()\n        )\n        editor.apply()\n        customEventsAndPurchasesArrayAdapter.clear()\n        customEventsAndPurchasesArrayAdapter.addAll(*lastSeenCustomEventsAndPurchases.toTypedArray())\n    }\n\n    private fun handleAliasClick() {\n        val alias = aliasEditText.text.toString()\n        val label = aliasLabelEditText.text.toString()\n        Braze.getInstance(requireContext()).getCurrentUser(object : IValueCallback<BrazeUser> {\n            override fun onSuccess(value: BrazeUser) {\n                value.addAlias(alias, label)\n                Toast.makeText(\n                    requireContext(),\n                    \"Added alias \" + alias + \" with label \"\n                        + label,\n                    Toast.LENGTH_SHORT\n                ).show()\n            }\n\n            override fun onError() {\n                Toast.makeText(requireContext(), \"Failed to add alias\", Toast.LENGTH_SHORT).show()\n            }\n        })\n    }\n\n    /**\n     * Shows a toast on the activity's UI thread\n     */\n    private fun showToast(msg: String) {\n        activity?.runOnUiThread { Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() }\n    }\n\n    companion object {\n        private const val STRING_ARRAY_ATTRIBUTE_KEY = \"stringArrayAttribute\"\n        private const val ARRAY_ATTRIBUTE_KEY = \"arrayAttribute\"\n        private const val DATE_ATTRIBUTE_KEY = \"dateAttribute\"\n        private const val PETS_ARRAY_ATTRIBUTE_KEY = \"arrayAttributePets\"\n        private const val FLOAT_ATTRIBUTE_KEY = \"floatAttribute\"\n        private const val BOOL_ATTRIBUTE_KEY = \"boolAttribute\"\n        private const val INT_ATTRIBUTE_KEY = \"intAttribute\"\n        private const val LONG_ATTRIBUTE_KEY = \"longAttribute\"\n        private const val STRING_ATTRIBUTE_KEY = \"stringAttribute\"\n        private const val DOUBLE_ATTRIBUTE_KEY = \"doubleAttribute\"\n        private const val INCREMENT_ATTRIBUTE_KEY = \"incrementAttribute\"\n        private const val ATTRIBUTION_DATA_KEY = \"ab_install_attribution\"\n        private const val LAST_SEEN_CUSTOM_EVENTS_AND_PURCHASES_PREFERENCE_KEY =\n            \"last_seen_custom_events_and_purchases\"\n        const val USER_ID_KEY = \"user.id\"\n\n        fun View.setOnButtonClick(id: Int, block: (view: View) -> Unit) {\n            val view = this.findViewById<Button>(id)\n            view.setOnClickListener { block(view) }\n        }\n\n        /**\n         * A combo of a button id and EditText id\n         */\n        fun View.setOnButtonClickWithEditableText(\n            buttonId: Int,\n            editTextId: Int,\n            block: (view: View, textValue: String) -> Unit\n        ) {\n            val view = this.findViewById<Button>(buttonId)\n            view.setOnClickListener {\n                val editTextValue = this.findViewById<EditText>(editTextId).text.toString()\n                block(view, editTextValue)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/PushTesterFragment.java",
    "content": "package com.appboy.sample;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.AdapterView;\nimport android.widget.Button;\nimport android.widget.CheckBox;\n\nimport androidx.fragment.app.Fragment;\n\nimport com.braze.Braze;\nimport com.braze.models.push.BrazeNotificationPayload;\nimport com.appboy.sample.util.SpinnerUtils;\nimport com.braze.Constants;\nimport com.braze.push.BrazeNotificationUtils;\nimport com.braze.push.BrazePushReceiver;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.StringUtils;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.security.SecureRandom;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@SuppressWarnings(\"PMD.AvoidDuplicateLiterals\")\npublic class PushTesterFragment extends Fragment implements AdapterView.OnItemSelectedListener {\n  protected static final String TAG = BrazeLogger.getBrazeLogTag(PushTesterFragment.class);\n  private static final String TITLE = \"Title\";\n  private static final String CONTENT = \"Content\";\n  private static final String BIG_TITLE = \"Big Title\";\n  private static final String BIG_SUMMARY = \"Big Summary\";\n  private static final String SUMMARY_TEXT = \"Summary Text\";\n  private static final SecureRandom sSecureRandom = new SecureRandom();\n  @SuppressLint(\"InlinedApi\")\n  private String mPriority = String.valueOf(0);\n  private String mImage;\n  private String mClickActionUrl;\n  private String mCategory;\n  private String mVisibility;\n  private String mActionType;\n  private String mAccentColorString;\n  private String mLargeIconString;\n  private String mNotificationFactoryType;\n  private String mPushStoryTitleGravity;\n  private String mPushStorySubtitleGravity;\n  private int mPushStoryType = 0;\n  private int mPushStoryNumPages;\n  private String mChannel;\n\n  private boolean mUseSummary = false;\n  private boolean mUseBigSummary = false;\n  private boolean mUseImage = false;\n  private boolean mShouldOverflowText = false;\n  private boolean mUseBigTitle = false;\n  private boolean mUseClickAction = false;\n  private boolean mUseCategory = false;\n  private boolean mUseVisibility = false;\n  private boolean mSetPublicVersion = false;\n  private boolean mSetAccentColor = false;\n  private boolean mSetLargeIcon = false;\n  private boolean mOpenInWebview = false;\n  private boolean mTestTriggerFetch = false;\n  private boolean mSetChannel = false;\n  private boolean mUseConstantNotificationId = false;\n  private boolean mStoryDeepLink = false;\n  private boolean mStoryTitles = true;\n  private boolean mStorySubtitles = true;\n  private boolean mInlineImagePushEnabled = false;\n  private boolean mConversationPushEnabled = false;\n  private boolean mSetHtmlText = false;\n  static final String EXAMPLE_APPBOY_EXTRA_KEY_1 = \"Entree\";\n  static final String EXAMPLE_APPBOY_EXTRA_KEY_2 = \"Side\";\n  static final String EXAMPLE_APPBOY_EXTRA_KEY_3 = \"Drink\";\n  private static final Map<Integer, String[]> PUSH_STORY_PAGE_VALUES;\n\n  static {\n    Map<Integer, String[]> pushStoryPageValues = new HashMap<>();\n    pushStoryPageValues.put(0, new String[]{\"http://appboy.com\", \"Twenty WWWWWWW WWWW#\", \"Twenty WWWWWWW WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de467360e39d4ac9c4b/original.jpeg?1623731684\"});\n    pushStoryPageValues.put(1, new String[]{\"http://google.com\", \"Twenty Five WW WWWW WWWW#\", \"Twenty Five WW WWWW WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de467360e2ab3ac9cf0/original.jpeg?1623731684\"});\n    pushStoryPageValues.put(2, new String[]{\"http://appboy.com\", \"Thirty WW WWWW WWWW WWWW WWWW#\", \"Thirty WW WWWW WWWW WWWW WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de4ad561022b6418bd8/original.jpeg?1623731684\"});\n    pushStoryPageValues.put(3, new String[]{\"http://appboy.com\", \"Forty WWW WWWW WWWW WWWW WWWW WWWW WWWW#\", \"Forty WWW WWWW WWWW WWWW WWWW WWWW WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de567360e3aa4ac9bed/original.jpeg?1623731685\"});\n    pushStoryPageValues.put(4, new String[]{\"http://appboy.com\", \"Forty Five WWW WWWW WWWW WWWW WWWW WWWW WWWW#\", \"Forty Five WWW WWWW WWWW WWWW WWWW WWWW WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de467360e7d35ac9e5f/original.jpeg?1623731684\"});\n    pushStoryPageValues.put(5, new String[]{\"http://appboy.com\", \"Fifteen W WWW#\", \"Fifteen W WWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de567360e2ab3ac9cf1/original.jpeg?1623731685\"});\n    pushStoryPageValues.put(6, new String[]{\"http://appboy.com\", \"Ten  WWWW#\", \"Ten  WWWW#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de53a531a3ff3e7ef46/original.jpeg?1623731685\"});\n    pushStoryPageValues.put(7, new String[]{\"http://appboy.com\", \"Five#\", \"Five#\", \"https://cdn-staging.braze.com/appboy/communication/assets/image_assets/images/60c82de5ad5610327e418ac2/original.jpeg?1623731684\"});\n    PUSH_STORY_PAGE_VALUES = pushStoryPageValues;\n  }\n\n  @Override\n  public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) {\n    View view = layoutInflater.inflate(R.layout.push_tester, container, false);\n\n    ((CheckBox) view.findViewById(R.id.push_tester_big_title)).setOnCheckedChangeListener((buttonView, isChecked) -> mUseBigTitle = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_summary)).setOnCheckedChangeListener((buttonView, isChecked) -> mUseSummary = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_big_summary)).setOnCheckedChangeListener((buttonView, isChecked) -> mUseBigSummary = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_overflow_text)).setOnCheckedChangeListener((buttonView, isChecked) -> mShouldOverflowText = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_set_public_version)).setOnCheckedChangeListener((buttonView, isChecked) -> mSetPublicVersion = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_test_triggers)).setOnCheckedChangeListener((buttonView, isChecked) -> mTestTriggerFetch = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_constant_nid)).setOnCheckedChangeListener((buttonView, isChecked) -> mUseConstantNotificationId = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_set_open_webview)).setOnCheckedChangeListener((buttonView, isChecked) -> mOpenInWebview = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_story_deep_link)).setOnCheckedChangeListener((buttonView, isChecked) -> mStoryDeepLink = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_story_title)).setOnCheckedChangeListener((buttonView, isChecked) -> mStoryTitles = !isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_story_subtitle)).setOnCheckedChangeListener((buttonView, isChecked) -> mStorySubtitles = !isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_inline_image_push_enabled)).setOnCheckedChangeListener((buttonView, isChecked) -> mInlineImagePushEnabled = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_conversational_push_enabled)).setOnCheckedChangeListener((buttonView, isChecked) -> mConversationPushEnabled = isChecked);\n    ((CheckBox) view.findViewById(R.id.push_tester_html)).setOnCheckedChangeListener((buttonView, isChecked) -> mSetHtmlText = isChecked);\n\n    // Creates the push image spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_image_spinner), this, R.array.push_image_options);\n\n    // Creates the push image number spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_image_number_spinner), this, R.array.push_image_number_options);\n\n    // Creates the push priority spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_priority_spinner), this, R.array.push_priority_options);\n\n    // Creates the push click action spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_click_action_spinner), this, R.array.push_click_action_options);\n\n    // Creates the notification category spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_category_spinner), this, R.array.push_category_options);\n\n    // Creates the visibility spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_visibility_spinner), this, R.array.push_visibility_options);\n\n    // Creates the push image spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_image_spinner), this, R.array.push_image_options);\n\n    // Creates the story title align spinner\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_story_title_align_spinner), this, R.array.push_story_title_align_options);\n\n    // Creates the story subtitle align spinner\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_story_subtitle_align_spinner), this, R.array.push_story_subtitle_align_options);\n\n    // Creates the push action spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_action_spinner), this, R.array.push_action_options);\n\n    // Creates the push accent color spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_accent_color_spinner), this, R.array.push_accent_color_options);\n\n    // Creates the large icon spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_large_icon_spinner), this, R.array.push_large_icon_options);\n\n    // Creates the notification factory spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_notification_factory_spinner), this, R.array.push_notification_factory_options);\n\n    // Creates the notification channel spinner.\n    SpinnerUtils.setUpSpinner(view.findViewById(R.id.push_channel_spinner), this, R.array.push_channel_options);\n\n    Button pushTestButton = view.findViewById(R.id.test_push_button);\n    pushTestButton.setOnClickListener(clickedView -> (new Thread(() -> {\n      Bundle notificationExtras = new Bundle();\n      if (mSetHtmlText) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_TITLE_KEY, getString(R.string.html_push_title_text));\n        notificationExtras.putString(Constants.BRAZE_PUSH_CONTENT_KEY, getString(R.string.html_push_body_text));\n      }\n      else {\n        notificationExtras.putString(Constants.BRAZE_PUSH_TITLE_KEY, generateDisplayValue(TITLE));\n        notificationExtras.putString(Constants.BRAZE_PUSH_CONTENT_KEY, generateDisplayValue(CONTENT + sSecureRandom.nextInt()));\n      }\n      notificationExtras.putString(Constants.BRAZE_PUSH_BRAZE_KEY, \"true\");\n\n      String notificationId;\n      if (mUseConstantNotificationId) {\n        notificationId = \"100\";\n      } else {\n        notificationId = String.valueOf(BrazeNotificationUtils.getNotificationId(new BrazeNotificationPayload(notificationExtras)));\n      }\n      notificationExtras.putString(Constants.BRAZE_PUSH_CUSTOM_NOTIFICATION_ID, notificationId);\n      notificationExtras = addActionButtons(notificationExtras);\n\n      if (mInlineImagePushEnabled) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_INLINE_IMAGE_STYLE_KEY, \"true\");\n      }\n      if (mUseSummary) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_SUMMARY_TEXT_KEY, generateDisplayValue(SUMMARY_TEXT));\n      }\n      if (mUseClickAction) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_DEEP_LINK_KEY, mClickActionUrl);\n      }\n      notificationExtras.putString(Constants.BRAZE_PUSH_PRIORITY_KEY, mPriority);\n      if (mUseBigTitle) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_BIG_TITLE_TEXT_KEY, generateDisplayValue(BIG_TITLE));\n      }\n      if (mUseBigSummary) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_BIG_SUMMARY_TEXT_KEY, generateDisplayValue(BIG_SUMMARY));\n      }\n      if (mUseCategory) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_CATEGORY_KEY, mCategory);\n      }\n      if (mUseVisibility) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_VISIBILITY_KEY, mVisibility);\n      }\n      if (mOpenInWebview) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_OPEN_URI_IN_WEBVIEW_KEY, \"true\");\n      }\n      if (mSetPublicVersion) {\n        try {\n          notificationExtras.putString(Constants.BRAZE_PUSH_PUBLIC_NOTIFICATION_KEY, getPublicVersionNotificationString());\n        } catch (JSONException jsonException) {\n          Log.e(TAG, \"Failed to created public version notification JSON string\", jsonException);\n        }\n      }\n      if (mTestTriggerFetch) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_FETCH_TEST_TRIGGERS_KEY, \"true\");\n      }\n      if (mSetAccentColor) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACCENT_KEY, mAccentColorString);\n      }\n      if (mSetLargeIcon) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_LARGE_ICON_KEY, mLargeIconString);\n      }\n      if (mSetChannel) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_NOTIFICATION_CHANNEL_ID_KEY, mChannel);\n      }\n      setNotificationFactory();\n\n      if (mPushStoryType != 0) {\n        addPushStoryPages(notificationExtras);\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_KEY, Integer.toString(mPushStoryType));\n      }\n      if (mConversationPushEnabled) {\n        notificationExtras.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_STYLE_KEY, \"1\");\n        addConversationPush(notificationExtras);\n      }\n\n      // Manually build the Braze extras bundle.\n      Bundle appboyExtras = new Bundle();\n      if (mUseImage) {\n        String pushImageUrl = mImage;\n        // Template the image if it's random\n        if (mImage.equals(getString(R.string.random_2_by_1_image_url))) {\n          pushImageUrl = \"https://picsum.photos/seed/\" + System.nanoTime() + \"/800/400\";\n        } else if (mImage.equals(getString(R.string.random_3_by_2_image_url))) {\n          pushImageUrl = \"https://picsum.photos/seed/\" + System.nanoTime() + \"/750/500\";\n        }\n\n        if (Constants.isAmazonDevice()) {\n          // Amazon flattens the extras bundle so we have to put it in the regular notification\n          // extras to imitate that functionality.\n          notificationExtras.putString(Constants.BRAZE_PUSH_BIG_IMAGE_URL_KEY, pushImageUrl.replaceAll(\"&amp;\", \"&\"));\n          appboyExtras = new Bundle(notificationExtras);\n        } else {\n          appboyExtras.putString(Constants.BRAZE_PUSH_BIG_IMAGE_URL_KEY, pushImageUrl.replaceAll(\"&amp;\", \"&\"));\n        }\n      }\n      appboyExtras.putString(EXAMPLE_APPBOY_EXTRA_KEY_1, \"Hamburger\");\n      appboyExtras.putString(EXAMPLE_APPBOY_EXTRA_KEY_2, \"Fries\");\n      appboyExtras.putString(EXAMPLE_APPBOY_EXTRA_KEY_3, \"Lemonade\");\n      notificationExtras.putBundle(Constants.BRAZE_PUSH_EXTRAS_KEY, appboyExtras);\n\n      Intent pushIntent = new Intent(BrazePushReceiver.FIREBASE_MESSAGING_SERVICE_ROUTING_ACTION);\n      pushIntent.putExtras(notificationExtras);\n      BrazePushReceiver.handleReceivedIntent(getContext(), pushIntent);\n    })).start());\n    return view;\n  }\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n  }\n\n  public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n    switch (parent.getId()) {\n      case R.id.push_image_spinner:\n        String pushImageUriString = getResources().getStringArray(R.array.push_image_values)[parent.getSelectedItemPosition()];\n        if (!StringUtils.isNullOrBlank(pushImageUriString)) {\n          if (pushImageUriString.equals(getString(R.string.push_story))) {\n            mPushStoryType = 1;\n            mUseImage = false;\n          } else {\n            mPushStoryType = 0;\n            mUseImage = true;\n            mImage = pushImageUriString;\n          }\n        } else {\n          mUseImage = false;\n          mPushStoryType = 0;\n        }\n        break;\n      case R.id.push_image_number_spinner:\n        String pushImageNumberString = getResources().getStringArray(R.array.push_image_number_values)[parent.getSelectedItemPosition()];\n        mPushStoryNumPages = Integer.parseInt(pushImageNumberString);\n        break;\n      case R.id.push_story_title_align_spinner:\n        mPushStoryTitleGravity = getResources().getStringArray(R.array.push_story_title_align_values)[parent.getSelectedItemPosition()];\n        break;\n      case R.id.push_story_subtitle_align_spinner:\n        mPushStorySubtitleGravity = getResources().getStringArray(R.array.push_story_subtitle_align_values)[parent.getSelectedItemPosition()];\n        break;\n      case R.id.push_priority_spinner:\n        mPriority = getResources().getStringArray(R.array.push_priority_values)[parent.getSelectedItemPosition()];\n        break;\n      case R.id.push_click_action_spinner:\n        String pushClickActionUriString = getResources().getStringArray(R.array.push_click_action_values)[parent.getSelectedItemPosition()];\n        if (!StringUtils.isNullOrBlank(pushClickActionUriString)) {\n          mUseClickAction = true;\n          mClickActionUrl = pushClickActionUriString;\n        } else {\n          mUseClickAction = false;\n        }\n        break;\n      case R.id.push_category_spinner:\n        mCategory = getResources().getStringArray(R.array.push_category_values)[parent.getSelectedItemPosition()];\n        mUseCategory = !StringUtils.isNullOrBlank(mCategory);\n        break;\n      case R.id.push_visibility_spinner:\n        mVisibility = getResources().getStringArray(R.array.push_visibility_values)[parent.getSelectedItemPosition()];\n        mUseVisibility = !StringUtils.isNullOrBlank(mVisibility);\n        break;\n      case R.id.push_action_spinner:\n        mActionType = getResources().getStringArray(R.array.push_action_values)[parent.getSelectedItemPosition()];\n        break;\n      case R.id.push_accent_color_spinner:\n        String pushAccentColorString = getResources().getStringArray(R.array.push_accent_color_values)[parent.getSelectedItemPosition()];\n        if (!StringUtils.isNullOrBlank(pushAccentColorString)) {\n          mSetAccentColor = true;\n          // Convert our hexadecimal string to the decimal expected by Braze\n          mAccentColorString = Long.decode(pushAccentColorString).toString();\n        } else {\n          mSetAccentColor = false;\n        }\n        break;\n      case R.id.push_large_icon_spinner:\n        String largeIconString = getResources().getStringArray(R.array.push_large_icon_values)[parent.getSelectedItemPosition()];\n        if (!StringUtils.isNullOrBlank(largeIconString)) {\n          mSetLargeIcon = true;\n          mLargeIconString = largeIconString;\n        } else {\n          mSetLargeIcon = false;\n        }\n        break;\n      case R.id.push_notification_factory_spinner:\n        mNotificationFactoryType = getResources().getStringArray(R.array.push_notification_factory_values)[parent.getSelectedItemPosition()];\n        break;\n      case R.id.push_channel_spinner:\n        mChannel = getResources().getStringArray(R.array.push_channel_values)[parent.getSelectedItemPosition()];\n        mSetChannel = !StringUtils.isNullOrBlank(mChannel);\n        break;\n      default:\n        Log.e(TAG, \"Item selected for unknown spinner\");\n    }\n  }\n\n  public void onNothingSelected(AdapterView<?> parent) {\n    // Do nothing\n  }\n\n  private String getPublicVersionNotificationString() throws JSONException {\n    JSONObject publicVersionJSON = new JSONObject();\n    publicVersionJSON.put(Constants.BRAZE_PUSH_TITLE_KEY, \"Don't open in public (title)\");\n    publicVersionJSON.put(Constants.BRAZE_PUSH_CONTENT_KEY, \"Please (content)\");\n    publicVersionJSON.put(Constants.BRAZE_PUSH_SUMMARY_TEXT_KEY, \"Summary\");\n    return publicVersionJSON.toString();\n  }\n\n  /**\n   * Add the push story fields to the notificationExtras bundle.\n   *\n   * @param notificationExtras Notification extras as provided by FCM/ADM.\n   * @return the modified notificationExtras, now including the image/text information for the push story.\n   */\n  private Bundle addPushStoryPages(Bundle notificationExtras) {\n    for (int i = 0; i < mPushStoryNumPages; i++) {\n      if (mStoryDeepLink) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_DEEP_LINK_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), PUSH_STORY_PAGE_VALUES.get(i)[0]);\n      }\n      if (mStoryTitles) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_TITLE_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), PUSH_STORY_PAGE_VALUES.get(i)[1]);\n      }\n      if (mStorySubtitles) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_SUBTITLE_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), PUSH_STORY_PAGE_VALUES.get(i)[2]);\n      }\n      notificationExtras.putString(Constants.BRAZE_PUSH_STORY_IMAGE_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), PUSH_STORY_PAGE_VALUES.get(i)[3]);\n      if (!StringUtils.isNullOrBlank(mPushStoryTitleGravity)) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_TITLE_JUSTIFICATION_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), mPushStoryTitleGravity);\n      }\n      if (!StringUtils.isNullOrBlank(mPushStorySubtitleGravity)) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_SUBTITLE_JUSTIFICATION_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), mPushStorySubtitleGravity);\n      }\n      if (mOpenInWebview) {\n        notificationExtras.putString(Constants.BRAZE_PUSH_STORY_USE_WEBVIEW_KEY_TEMPLATE.replace(\"*\", Integer.toString(i)), \"true\");\n      }\n    }\n    notificationExtras.putBoolean(Constants.BRAZE_PUSH_STORY_IS_NEWLY_RECEIVED, true);\n    return notificationExtras;\n  }\n\n  private Bundle addActionButtons(Bundle notificationExtras) {\n    if (StringUtils.isNullOrBlank(mActionType)) {\n      return notificationExtras;\n    }\n    switch (mActionType) {\n      case Constants.BRAZE_PUSH_ACTION_TYPE_OPEN:\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"0\"), Constants.BRAZE_PUSH_ACTION_TYPE_OPEN);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"0\"), \"Open app\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"1\"), Constants.BRAZE_PUSH_ACTION_TYPE_NONE);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"1\"), getString(R.string.droidboy_close_button_text));\n        break;\n      case Constants.BRAZE_PUSH_ACTION_TYPE_URI:\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"0\"), Constants.BRAZE_PUSH_ACTION_TYPE_URI);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"0\"), \"Braze (webview)\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_URI_KEY_TEMPLATE.replace(\"*\", \"0\"), getString(R.string.braze_homepage_url));\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_USE_WEBVIEW_KEY_TEMPLATE.replace(\"*\", \"0\"), \"true\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"1\"), Constants.BRAZE_PUSH_ACTION_TYPE_URI);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"1\"), \"Google\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_URI_KEY_TEMPLATE.replace(\"*\", \"1\"), getString(R.string.google_url));\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_USE_WEBVIEW_KEY_TEMPLATE.replace(\"*\", \"1\"), \"false\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"2\"), Constants.BRAZE_PUSH_ACTION_TYPE_NONE);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"2\"), getString(R.string.droidboy_close_button_text));\n        if (mOpenInWebview) {\n          notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_USE_WEBVIEW_KEY_TEMPLATE.replace(\"*\", \"0\"), \"true\");\n          notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_USE_WEBVIEW_KEY_TEMPLATE.replace(\"*\", \"1\"), \"true\");\n        }\n        break;\n      case \"deep_link\":\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"0\"), Constants.BRAZE_PUSH_ACTION_TYPE_URI);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"0\"), \"Preferences\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_URI_KEY_TEMPLATE.replace(\"*\", \"0\"), getString(R.string.droidboy_deep_link));\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"1\"), Constants.BRAZE_PUSH_ACTION_TYPE_URI);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"1\"), \"Telephone\");\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_URI_KEY_TEMPLATE.replace(\"*\", \"1\"), getString(R.string.telephone_uri));\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TYPE_KEY_TEMPLATE.replace(\"*\", \"2\"), Constants.BRAZE_PUSH_ACTION_TYPE_NONE);\n        notificationExtras.putString(Constants.BRAZE_PUSH_ACTION_TEXT_KEY_TEMPLATE.replace(\"*\", \"2\"), getString(R.string.droidboy_close_button_text));\n        break;\n      default:\n    }\n    return notificationExtras;\n  }\n\n  private void addConversationPush(Bundle bundle) {\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_SHORTCUT_ID_KEY, \"droidboy_dynamic_shortcut_chat_id\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_REPLY_PERSON_ID_KEY, \"person2\");\n\n    // Add messages\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_TEXT_TEMPLATE.replace(\"*\", \"0\"), \"Message 1\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_PERSON_ID_TEMPLATE.replace(\"*\", \"0\"), \"person1\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_TIMESTAMP_TEMPLATE.replace(\"*\", \"0\"), String.valueOf(System.currentTimeMillis() - 3600));\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_TEXT_TEMPLATE.replace(\"*\", \"1\"), \"Message 2\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_PERSON_ID_TEMPLATE.replace(\"*\", \"1\"), \"person2\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_MESSAGE_TIMESTAMP_TEMPLATE.replace(\"*\", \"1\"), String.valueOf(System.currentTimeMillis()));\n\n    // Add persons\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_ID_TEMPLATE.replace(\"*\", \"0\"), \"person1\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_NAME_TEMPLATE.replace(\"*\", \"0\"), \"Jack Black\");\n\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_ID_TEMPLATE.replace(\"*\", \"1\"), \"person2\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_NAME_TEMPLATE.replace(\"*\", \"1\"), \"Giraffe\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_URI_TEMPLATE.replace(\"*\", \"1\"), \"mailto://giraffe@zoo.org\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_IS_BOT_TEMPLATE.replace(\"*\", \"1\"), \"true\");\n    bundle.putString(Constants.BRAZE_CONVERSATIONAL_PUSH_PERSON_IS_IMPORTANT_TEMPLATE.replace(\"*\", \"1\"), \"true\");\n  }\n\n  // If shouldOverflowText is specified we concatenate an append string\n  // This is to test big te8xt and ellipsis cutoff in varying screen sizes\n  private String generateDisplayValue(String field) {\n    if (mShouldOverflowText) {\n      return field + getString(R.string.overflow_string);\n    }\n    return field;\n  }\n\n  /**\n   * Sets the Braze instance's notification factory.\n   */\n  private void setNotificationFactory() {\n    if (\"DroidboyNotificationFactory\".equals(mNotificationFactoryType)) {\n      Braze.setCustomBrazeNotificationFactory(new DroidboyNotificationFactory());\n    } else if (\"FullyCustomNotificationFactory\".equals(mNotificationFactoryType)) {\n      Braze.setCustomBrazeNotificationFactory(new FullyCustomNotificationFactory());\n    } else {\n      Braze.setCustomBrazeNotificationFactory(null);\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/SetEnvironmentPreference.java",
    "content": "package com.appboy.sample;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.dialog.CustomDialogBase;\nimport com.appboy.sample.util.LifecycleUtils;\n\nimport java.util.Map;\n\npublic class SetEnvironmentPreference extends CustomDialogBase {\n  private static final String OVERRIDE_API_KEY_ALIAS_PREF_KEY = \"override_api_key_alias\";\n\n  private TextView mApiKeyAliasTextView;\n  private TextView mApiKeyTextView;\n  private TextView mEndpointTextView;\n  private Context mApplicationContext;\n  private SharedPreferences mSharedPreferences;\n  private SharedPreferences mApiKeySharedPreferences;\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    mApplicationContext = getContext().getApplicationContext();\n    return inflater.inflate(R.layout.set_environment_preference, container, false);\n  }\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n    mSharedPreferences = mApplicationContext.getSharedPreferences(mApplicationContext.getString(R.string.shared_prefs_location), Context.MODE_PRIVATE);\n    String overrideApiKeyAlias = mSharedPreferences.getString(OVERRIDE_API_KEY_ALIAS_PREF_KEY, null);\n    String overrideApiKey = mSharedPreferences.getString(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY, null);\n    String overrideEndpointUrl = mSharedPreferences.getString(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY, null);\n\n    mApiKeyAliasTextView = view.findViewById(R.id.set_environment_override_api_key_alias);\n    mApiKeyTextView = view.findViewById(R.id.set_environment_override_api_key);\n    mEndpointTextView = view.findViewById(R.id.set_environment_override_endpoint_url);\n    if (overrideApiKeyAlias != null) {\n      mApiKeyAliasTextView.setText(overrideApiKeyAlias);\n    }\n    if (overrideApiKey != null) {\n      mApiKeyTextView.setText(overrideApiKey);\n    }\n    if (overrideEndpointUrl != null) {\n      mEndpointTextView.setText(overrideEndpointUrl);\n    }\n\n    LinearLayout storedApiKeyLinearLayout = view.findViewById(R.id.stored_api_key_layout);\n\n    mApiKeySharedPreferences = mApplicationContext.getSharedPreferences(mApplicationContext.getString(R.string.api_key_shared_prefs_location), Context.MODE_PRIVATE);\n    Map<String, ?> apiKeys = mApiKeySharedPreferences.getAll();\n\n    // populate default API key\n    if (!apiKeys.containsKey(\"Default\")) {\n      String appboyXmlApiKey = DroidboyApplication.getApiKeyInUse(getContext());\n      storedApiKeyLinearLayout.addView(getApiKeyButton(\"Default\", appboyXmlApiKey));\n    }\n    // populate previously stored API keys\n    for (final String alias : apiKeys.keySet()) {\n      final String apiKey = mApiKeySharedPreferences.getString(alias, null);\n      storedApiKeyLinearLayout.addView(getApiKeyButton(alias, apiKey));\n    }\n  }\n\n  private Button getApiKeyButton(final String alias, final String apiKey) {\n    Button button = new Button(getContext());\n    button.setOnClickListener(\n        view -> {\n          mApiKeyAliasTextView.setText(alias);\n          mApiKeyTextView.setText(apiKey);\n        }\n    );\n    button.setText(alias + \": \" + apiKey);\n    return button;\n  }\n\n  @Override\n  @SuppressLint({\"CommitPrefEdits\", \"ApplySharedPref\"})\n  public void onExitButtonPressed(boolean clickedPositiveButton) {\n    if (clickedPositiveButton) {\n      SharedPreferences.Editor sharedPreferencesEditor = mSharedPreferences.edit();\n      String apiKeyAlias = mApiKeyAliasTextView.getText().toString();\n      String apiKey = mApiKeyTextView.getText().toString();\n      String endpoint = mEndpointTextView.getText().toString();\n      if (apiKeyAlias.length() > 0) {\n        sharedPreferencesEditor.putString(OVERRIDE_API_KEY_ALIAS_PREF_KEY, apiKeyAlias);\n      } else {\n        sharedPreferencesEditor.remove(OVERRIDE_API_KEY_ALIAS_PREF_KEY);\n      }\n      if (apiKey.length() > 0) {\n        sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY, apiKey);\n      } else {\n        sharedPreferencesEditor.remove(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY);\n      }\n      if (apiKeyAlias.length() > 0 && apiKey.length() > 0) {\n        mApiKeySharedPreferences.edit().putString(apiKeyAlias, apiKey).commit();\n      }\n      if (endpoint.length() > 0) {\n        sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY, endpoint);\n      } else {\n        sharedPreferencesEditor.remove(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY);\n      }\n\n      sharedPreferencesEditor.commit();\n      LifecycleUtils.restartApp(mApplicationContext);\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/UserProfileDialog.java",
    "content": "package com.appboy.sample;\n\nimport android.app.DatePickerDialog;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.RadioGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.braze.enums.Gender;\nimport com.braze.enums.Month;\nimport com.appboy.sample.dialog.CustomDialogBase;\nimport com.appboy.sample.util.ButtonUtils;\nimport com.braze.Braze;\nimport com.braze.BrazeUser;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.StringUtils;\n\nimport java.util.Calendar;\n\npublic class UserProfileDialog extends CustomDialogBase implements View.OnClickListener {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(UserProfileDialog.class);\n  private static final int GENDER_UNSPECIFIED_INDEX = 0;\n  private static final int GENDER_MALE_INDEX = 1;\n  private static final int GENDER_FEMALE_INDEX = 2;\n  private static final int GENDER_OTHER_INDEX = 3;\n  private static final int GENDER_UNKNOWN_INDEX = 4;\n  private static final int GENDER_NOT_APPLICABLE_INDEX = 5;\n  private static final int GENDER_PREFER_NOT_TO_SAY_INDEX = 6;\n\n  private static final Calendar mCalendar = Calendar.getInstance();\n\n  private static final String FIRST_NAME_PREFERENCE_KEY = \"user.firstname\";\n  private static final String LAST_NAME_PREFERENCE_KEY = \"user.lastname\";\n  private static final String LANGUAGE_PREFERENCE_KEY = \"user.language\";\n  private static final String EMAIL_PREFERENCE_KEY = \"user.email\";\n  private static final String GENDER_PREFERENCE_KEY = \"user.gender_resource_id\";\n  private static final String BIRTHDAY_PREFERENCE_KEY = \"user.birthday\";\n\n  private static final String SAMPLE_FIRST_NAME = \"Jane\";\n  private static final String SAMPLE_LAST_NAME = \"Doe\";\n  private static final String SAMPLE_LANGUAGE = \"hi\";\n  private static final String SAMPLE_EMAIL = \"jane@appboy.com\";\n  private static final int SAMPLE_GENDER = R.id.female;\n  private static final String SAMPLE_BIRTHDAY = (mCalendar.get(Calendar.MONTH) + 1) + \"/\" + mCalendar.get(Calendar.DAY_OF_MONTH) + \"/\" + mCalendar.get(Calendar.YEAR);\n\n  private EditText mFirstName;\n  private EditText mLastName;\n  private EditText mEmail;\n  private RadioGroup mGender;\n  private EditText mLanguage;\n  private TextView mBirthday;\n\n  private DatePickerDialog mDatePickerDialog;\n  private int mBirthYear;\n  private int mBirthMonth;\n  private int mBirthDay;\n  private boolean isBirthdaySet = false;\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    final View view = inflater.inflate(R.layout.user_preferences, container, false);\n    mFirstName = view.findViewById(R.id.first_name);\n    mLastName = view.findViewById(R.id.last_name);\n    mEmail = view.findViewById(R.id.email);\n    mGender = view.findViewById(R.id.gender);\n    mLanguage = view.findViewById(R.id.language);\n    mBirthday = view.findViewById(R.id.birthday);\n    return view;\n  }\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n\n    SharedPreferences sharedPreferences = getSharedPreferences();\n    mFirstName.setText(sharedPreferences.getString(FIRST_NAME_PREFERENCE_KEY, null));\n    mLastName.setText(sharedPreferences.getString(LAST_NAME_PREFERENCE_KEY, null));\n    mEmail.setText(sharedPreferences.getString(EMAIL_PREFERENCE_KEY, null));\n    mGender.check(parseGenderFromSharedPreferences());\n    mLanguage.setText(sharedPreferences.getString(LANGUAGE_PREFERENCE_KEY, null));\n    mBirthday.setText(sharedPreferences.getString(BIRTHDAY_PREFERENCE_KEY, null));\n\n    ButtonUtils.setUpPopulateButton(view, R.id.first_name_button, mFirstName, getSharedPreferences().getString(FIRST_NAME_PREFERENCE_KEY, SAMPLE_FIRST_NAME));\n    ButtonUtils.setUpPopulateButton(view, R.id.last_name_button, mLastName, getSharedPreferences().getString(LAST_NAME_PREFERENCE_KEY, SAMPLE_LAST_NAME));\n    ButtonUtils.setUpPopulateButton(view, R.id.email_button, mEmail, getSharedPreferences().getString(EMAIL_PREFERENCE_KEY, SAMPLE_EMAIL));\n    ButtonUtils.setUpPopulateButton(view, R.id.language_button, mLanguage, getSharedPreferences().getString(LANGUAGE_PREFERENCE_KEY, SAMPLE_LANGUAGE));\n\n    final Button populateButton = view.findViewById(R.id.user_dialog_button_populate);\n    final Button clearButton = view.findViewById(R.id.user_dialog_button_clear);\n    final Button birthdayButton = view.findViewById(R.id.birthday_button);\n\n    populateButton.setOnClickListener(this);\n    clearButton.setOnClickListener(this);\n    birthdayButton.setOnClickListener(this);\n\n    mDatePickerDialog = new DatePickerDialog(getContext(), (view1, year, monthOfYear, dayOfMonth) -> {\n      mBirthYear = year;\n      mBirthMonth = monthOfYear;\n      mBirthDay = dayOfMonth;\n      mBirthday.setText(getBirthday());\n      isBirthdaySet = true;\n    }, mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));\n  }\n\n  @Override\n  public void onClick(View view) {\n    switch (view.getId()) {\n      case R.id.user_dialog_button_clear:\n        clear();\n        break;\n      case R.id.user_dialog_button_populate:\n        populate();\n        break;\n      case R.id.birthday_button:\n        mDatePickerDialog.show();\n        break;\n      default:\n        break;\n    }\n  }\n\n  private void clear() {\n    mFirstName.getText().clear();\n    mLastName.getText().clear();\n    mEmail.getText().clear();\n    mGender.check(R.id.unspecified);\n    mBirthday.setText(\"\");\n    isBirthdaySet = false;\n  }\n\n  private void populate() {\n    if (mFirstName.getText().length() == 0) {\n      mFirstName.setText(getSharedPreferences().getString(FIRST_NAME_PREFERENCE_KEY, SAMPLE_FIRST_NAME));\n    }\n    if (mLastName.getText().length() == 0) {\n      mLastName.setText(getSharedPreferences().getString(LAST_NAME_PREFERENCE_KEY, SAMPLE_LAST_NAME));\n    }\n    if (mLanguage.getText().length() == 0) {\n      mLanguage.setText(getSharedPreferences().getString(LANGUAGE_PREFERENCE_KEY, SAMPLE_LANGUAGE));\n    }\n    if (mEmail.getText().length() == 0) {\n      mEmail.setText(getSharedPreferences().getString(EMAIL_PREFERENCE_KEY, SAMPLE_EMAIL));\n    }\n    if (mGender.getCheckedRadioButtonId() == R.id.unspecified) {\n      mGender.check(SAMPLE_GENDER);\n    }\n    if (mBirthday.getText().length() == 0) {\n      mBirthday.setText(getSharedPreferences().getString(BIRTHDAY_PREFERENCE_KEY, SAMPLE_BIRTHDAY));\n      isBirthdaySet = true;\n    }\n  }\n\n  @Override\n  public void onExitButtonPressed(boolean isPositive) {\n    String firstName = mFirstName.getText().toString();\n    String lastName = mLastName.getText().toString();\n    String email = mEmail.getText().toString();\n    int genderResourceId = mGender.getCheckedRadioButtonId();\n    View genderRadioButton = mGender.findViewById(genderResourceId);\n    int genderId = mGender.indexOfChild(genderRadioButton);\n    String language = mLanguage.getText().toString();\n\n    BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n    SharedPreferences.Editor editor = getSharedPreferences().edit();\n    if (!StringUtils.isNullOrBlank(firstName)) {\n      brazeUser.setFirstName(firstName);\n      editor.putString(FIRST_NAME_PREFERENCE_KEY, firstName);\n    }\n    if (!StringUtils.isNullOrBlank(lastName)) {\n      brazeUser.setLastName(lastName);\n      editor.putString(LAST_NAME_PREFERENCE_KEY, lastName);\n    }\n    if (!StringUtils.isNullOrBlank(language)) {\n      brazeUser.setLanguage(language);\n      editor.putString(LANGUAGE_PREFERENCE_KEY, language);\n    }\n    if (!StringUtils.isNullOrBlank(email)) {\n      editor.putString(EMAIL_PREFERENCE_KEY, email);\n      brazeUser.setEmail(email);\n    }\n    if (isBirthdaySet) {\n      editor.putString(BIRTHDAY_PREFERENCE_KEY, getBirthday());\n      brazeUser.setDateOfBirth(mBirthYear, Month.getMonth(mBirthMonth), mBirthDay);\n    }\n\n    switch (genderId) {\n      case GENDER_UNSPECIFIED_INDEX:\n        brazeUser.setGender(null);\n        break;\n      case GENDER_MALE_INDEX:\n        brazeUser.setGender(Gender.MALE);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      case GENDER_FEMALE_INDEX:\n        brazeUser.setGender(Gender.FEMALE);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      case GENDER_OTHER_INDEX:\n        brazeUser.setGender(Gender.OTHER);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      case GENDER_UNKNOWN_INDEX:\n        brazeUser.setGender(Gender.UNKNOWN);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      case GENDER_NOT_APPLICABLE_INDEX:\n        brazeUser.setGender(Gender.NOT_APPLICABLE);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      case GENDER_PREFER_NOT_TO_SAY_INDEX:\n        brazeUser.setGender(Gender.PREFER_NOT_TO_SAY);\n        editor.putInt(GENDER_PREFERENCE_KEY, genderId);\n        break;\n      default:\n        Log.w(TAG, \"Error parsing gender from user preferences.\");\n    }\n    editor.apply();\n\n    // Flushing manually is not recommended in almost all production situations as\n    // Braze automatically flushes data to its servers periodically. This call\n    // is solely for testing purposes.\n    if (isPositive) {\n      Braze.getInstance(getContext()).requestImmediateDataFlush();\n    }\n    this.dismiss();\n  }\n\n  private String getBirthday() {\n    return (mBirthMonth + 1) + \"/\" + mBirthDay + \"/\" + mBirthYear;\n  }\n\n  private int parseGenderFromSharedPreferences() {\n    switch (getSharedPreferences().getInt(GENDER_PREFERENCE_KEY, GENDER_UNSPECIFIED_INDEX)) {\n      case GENDER_UNSPECIFIED_INDEX:\n        return R.id.unspecified;\n      case GENDER_MALE_INDEX:\n        return R.id.male;\n      case GENDER_FEMALE_INDEX:\n        return R.id.female;\n      case GENDER_OTHER_INDEX:\n        return R.id.other;\n      case GENDER_UNKNOWN_INDEX:\n        return R.id.unknown;\n      case GENDER_NOT_APPLICABLE_INDEX:\n        return R.id.not_applicable;\n      case GENDER_PREFER_NOT_TO_SAY_INDEX:\n        return R.id.prefer_not_to_say;\n      default:\n        Log.w(TAG, \"Error parsing gender from shared preferences.\");\n        return R.id.unspecified;\n    }\n  }\n\n  private SharedPreferences getSharedPreferences() {\n    return this.getContext().getSharedPreferences(getString(R.string.shared_prefs_location), Context.MODE_PRIVATE);\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/DroidBoyActivity.kt",
    "content": "package com.appboy.sample.activity\n\nimport android.Manifest\nimport android.content.Context\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.inputmethod.InputMethodManager\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport androidx.drawerlayout.widget.DrawerLayout\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentManager\nimport androidx.lifecycle.Lifecycle\nimport androidx.preference.PreferenceManager\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport androidx.viewpager2.widget.ViewPager2\nimport com.appboy.sample.featureflag.view.FeatureFlagFragment\nimport com.appboy.sample.FeedCategoriesFragment\nimport com.appboy.sample.FeedCategoriesFragment.NoticeDialogListener\nimport com.appboy.sample.InAppMessageTesterFragment\nimport com.appboy.sample.MainFragment\nimport com.appboy.sample.PushTesterFragment\nimport com.appboy.sample.R\nimport com.appboy.sample.activity.settings.SettingsFragment\nimport com.appboy.sample.util.RuntimePermissionUtils.requestLocationPermissions\nimport com.appboy.sample.util.ViewUtils\nimport com.braze.Braze\nimport com.braze.Constants\nimport com.braze.configuration.BrazeConfigurationProvider\nimport com.braze.enums.CardCategory\nimport com.braze.events.IEventSubscriber\nimport com.braze.events.NoMatchingTriggerEvent\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.support.hasPermission\nimport com.braze.ui.BrazeFeedFragment\nimport com.braze.ui.contentcards.ContentCardsFragment\nimport com.google.android.material.tabs.TabLayout\nimport com.google.android.material.tabs.TabLayoutMediator\nimport java.util.*\n\nclass DroidBoyActivity : AppCompatActivity(), NoticeDialogListener {\n    private var feedCategories: EnumSet<CardCategory>? = null\n    private var drawerLayout: DrawerLayout? = null\n    private var noMatchingTriggerEventSubscriber: IEventSubscriber<NoMatchingTriggerEvent>? = null\n    private var noInAppMessageTriggeredPrefListener: SharedPreferences.OnSharedPreferenceChangeListener? = null\n\n    private val requestMultiplePermissionLauncher = registerForActivityResult(RequestMultiplePermissions()) { result: Map<String, Boolean> ->\n        if (result.containsKey(Manifest.permission.ACCESS_FINE_LOCATION)\n            && result[Manifest.permission.ACCESS_FINE_LOCATION] != true\n        ) {\n            showToast(\"Location permissions denied.\")\n        } else if (result.containsKey(Manifest.permission.ACCESS_BACKGROUND_LOCATION)\n            && result[Manifest.permission.ACCESS_BACKGROUND_LOCATION] != true\n        ) {\n            showToast(\"Background location permissions denied.\")\n        } else {\n            showToast(\"All required location permissions granted.\")\n        }\n    }\n\n    private val feedFragment: BrazeFeedFragment?\n        get() {\n            val fragments = supportFragmentManager.fragments\n            for (i in fragments.indices) {\n                if (fragments[i] is BrazeFeedFragment) {\n                    return fragments[i] as BrazeFeedFragment?\n                }\n            }\n            return null\n        }\n\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        checkPermissions()\n        val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)\n\n        if (defaultSharedPreferences.getBoolean(\"display_in_full_cutout_setting_key\", false)) {\n            setTheme(R.style.DisplayInNotchTheme)\n            ViewUtils.enableImmersiveMode(window.decorView)\n        }\n\n        if (defaultSharedPreferences.getBoolean(\"display_no_limits_setting_key\", false)) {\n            ViewUtils.enableNoLimitsMode(window)\n        }\n        setContentView(R.layout.landing_page)\n        val currentFragment = supportFragmentManager.findFragmentById(R.id.viewpager)\n        brazelog(I) { \"Creating DroidBoyActivity with current fragment: $currentFragment\" }\n        val toolbar = findViewById<Toolbar>(R.id.toolbar)\n        setSupportActionBar(toolbar)\n        supportActionBar?.title = null\n        val viewPager = findViewById<ViewPager2>(R.id.viewpager)\n        viewPager?.let { setupViewPager(it) }\n\n        val tabLayout = findViewById<TabLayout>(R.id.tabs)\n        TabLayoutMediator(tabLayout, viewPager) { tab, position ->\n            tab.text = (viewPager.adapter as Adapter).getTitle(position)\n        }.attach()\n        drawerLayout = findViewById(R.id.root)\n        setupNewsFeedListener()\n        setupNoInAppMessageTriggeredListener()\n        brazelog(I) { \"Braze device id is ${Braze.getInstance(applicationContext).deviceId}\" }\n    }\n\n    private fun setupNewsFeedListener() {\n        val newsFeedSharedPrefs = getSharedPreferences(getString(R.string.feed), MODE_PRIVATE)\n        // We implement the listener this way so that it doesn't get garbage collected when we navigate to and from this activity\n        val newsfeedSortListener =\n            SharedPreferences.OnSharedPreferenceChangeListener { _: SharedPreferences?, _: String? ->\n                val sharedPref1 = getSharedPreferences(getString(R.string.feed), MODE_PRIVATE)\n                feedFragment?.let {\n                    it.sortEnabled = sharedPref1.getBoolean(getString(R.string.sort_feed), false)\n                }\n            }\n        newsFeedSharedPrefs.registerOnSharedPreferenceChangeListener(newsfeedSortListener)\n    }\n\n    private fun setupNoInAppMessageTriggeredListener() {\n        fun handlePref(value: Boolean) {\n            if (value) {\n                if (noMatchingTriggerEventSubscriber == null) {\n                    noMatchingTriggerEventSubscriber = IEventSubscriber { message ->\n                        runOnUiThread {\n                            // A simple non-Braze created message that we do on our own\n                            val dialog = AlertDialog.Builder(this)\n                                .setMessage(\"Received no trigger for ${message.sourceEventType}\")\n                                .setTitle(\"Non-Braze Message\")\n                                .setPositiveButton(R.string.user_dialog_okay) { _, _ -> }\n                                .create()\n                            dialog.show()\n                        }\n                    }\n                    noMatchingTriggerEventSubscriber?.let {\n                        Braze.getInstance(this)\n                            .subscribeToNoMatchingTriggerForEvent(it)\n                    }\n                }\n            } else if (noMatchingTriggerEventSubscriber != null) {\n                noMatchingTriggerEventSubscriber?.let {\n                    Braze.getInstance(this).removeSingleSubscription(it, NoMatchingTriggerEvent::class.java)\n                }\n                noMatchingTriggerEventSubscriber = null\n            }\n        }\n\n        val inAppMessagesSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)\n\n        noInAppMessageTriggeredPrefListener =\n            SharedPreferences.OnSharedPreferenceChangeListener { sharedPrefs: SharedPreferences, prefName: String? ->\n                if (prefName != \"show_own_message_when_inapp_msg_not_triggered\") {\n                    return@OnSharedPreferenceChangeListener\n                }\n                handlePref(\n                    sharedPrefs.getBoolean(\n                        \"show_own_message_when_inapp_msg_not_triggered\",\n                        false\n                    )\n                )\n            }\n        inAppMessagesSharedPrefs.registerOnSharedPreferenceChangeListener(\n            noInAppMessageTriggeredPrefListener\n        )\n        handlePref(\n            inAppMessagesSharedPrefs.getBoolean(\n                \"show_own_message_when_inapp_msg_not_triggered\",\n                false\n            )\n        )\n    }\n\n    private fun checkPermissions() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return\n\n        val permissionsToRequest = mutableListOf(Manifest.permission.INTERNET)\n        if (!didRequestLocationPermission) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n                val hasFineLocationPermission =\n                    applicationContext.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)\n                if (!hasFineLocationPermission) {\n                    permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)\n                } else if (!applicationContext.hasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {\n                    // Request background now that fine is set\n                    permissionsToRequest.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)\n                }\n            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                val hasAllPermissions = (\n                    applicationContext.hasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)\n                        && applicationContext.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)\n                    )\n                if (!hasAllPermissions) {\n                    // Request both BACKGROUND and FINE location permissions\n                    permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)\n                    permissionsToRequest.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)\n                }\n            } else {\n                // From M to P, FINE gives us BACKGROUND access\n                if (!applicationContext.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {\n                    // Request only FINE location permission\n                    permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)\n                }\n            }\n            didRequestLocationPermission = true\n        }\n        requestLocationPermissions(\n            this,\n            permissionsToRequest.toTypedArray(),\n            requestMultiplePermissionLauncher\n        )\n    }\n\n    private fun setupViewPager(viewPager: ViewPager2) {\n        viewPager.adapter = Adapter(supportFragmentManager, lifecycle, applicationContext)\n    }\n\n    public override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        setIntent(intent)\n    }\n\n    public override fun onResume() {\n        super.onResume()\n        processIntent()\n        val configurationProvider = BrazeConfigurationProvider(this)\n        val endpoint = configurationProvider.customEndpoint.let {\n            if (it.isNullOrEmpty()) {\n                configurationProvider.baseUrlForRequests\n            } else {\n                it\n            }\n        }\n\n        (findViewById<View>(R.id.toolbar_info_endpoint) as TextView).text = \"endpoint: $endpoint\"\n        val configuredApiKey = Braze.getConfiguredApiKey(configurationProvider)\n        (findViewById<View>(R.id.toolbar_info_api_key) as TextView).text = \"current api key: $configuredApiKey\"\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu): Boolean {\n        menuInflater.inflate(R.menu.actionbar_options, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.feed_activity_launch -> startActivity(Intent(this, FeedFragmentActivity::class.java))\n            R.id.action_settings -> startActivity(Intent(this, SettingsActivity::class.java))\n            R.id.geofences_map -> {\n                drawerLayout?.closeDrawers()\n                startActivity(Intent(applicationContext, GeofencesMapActivity::class.java))\n            }\n            R.id.iam_sandbox -> startActivity(Intent(applicationContext, InAppMessageSandboxActivity::class.java))\n            R.id.feed_categories -> {\n                val feedFragment = feedFragment\n                if (feedFragment != null) {\n                    val newFragment: DialogFragment = FeedCategoriesFragment.newInstance(feedFragment.categories ?: CardCategory.getAllCategories())\n                    newFragment.show(supportFragmentManager, \"categories\")\n                } else {\n                    showToast(\"Feed fragment hasn't been instantiated yet.\")\n                }\n            }\n            R.id.action_flush -> {\n                Braze.getInstance(this).requestContentCardsRefresh(false)\n                Braze.getInstance(this).requestImmediateDataFlush()\n                showToast(\"Requested data flush and content card sync.\")\n            }\n            else -> brazelog(E) { \"The ${item.title} options item was not found. Ignoring.\" }\n        }\n        return true\n    }\n\n    private fun replaceCurrentFragment(newFragment: Fragment) {\n        val fragmentManager = supportFragmentManager\n        val currentFragment = fragmentManager.findFragmentById(R.id.root)\n        if (currentFragment != null && currentFragment.javaClass == newFragment.javaClass) {\n            brazelog(I) {\n                \"Fragment of type ${currentFragment.javaClass} is already the active fragment. Ignoring \" +\n                    \"request to replace current fragment.\"\n            }\n            return\n        }\n        hideSoftKeyboard()\n        val fragmentTransaction = fragmentManager.beginTransaction()\n        fragmentTransaction.setCustomAnimations(\n            android.R.anim.fade_in, android.R.anim.fade_out,\n            android.R.anim.fade_in, android.R.anim.fade_out\n        )\n        fragmentTransaction.replace(R.id.root, newFragment, newFragment.javaClass.toString())\n        if (currentFragment != null) {\n            fragmentTransaction.addToBackStack(newFragment.javaClass.toString())\n        } else {\n            fragmentTransaction.addToBackStack(null)\n        }\n        fragmentTransaction.commit()\n    }\n\n    private fun processIntent() {\n        // Check to see if the Activity was opened by the Broadcast Receiver. If it was, navigate to the\n        // correct fragment.\n        val extras = intent.extras\n        if (extras != null && Constants.BRAZE == extras.getString(resources.getString(R.string.source_key))) {\n            navigateToDestination(extras)\n            val bundleLogString = bundleToLogString(extras)\n            showToast(bundleLogString)\n            brazelog { bundleLogString }\n        }\n\n        // Clear the intent so that screen rotations don't cause the intent to be re-executed on.\n        intent = Intent()\n    }\n\n    private fun navigateToDestination(extras: Bundle) {\n        // DESTINATION_VIEW holds the name of the fragment we're trying to visit.\n        val destination = extras.getString(resources.getString(R.string.destination_view))\n        if (resources.getString(R.string.feed_key) == destination) {\n            val feedFragment = BrazeFeedFragment()\n            feedFragment.categories = feedCategories\n            replaceCurrentFragment(feedFragment)\n        } else if (resources.getString(R.string.home) == destination) {\n            replaceCurrentFragment(MainFragment())\n        }\n    }\n\n    private fun hideSoftKeyboard() {\n        currentFocus?.let {\n            val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager\n            inputMethodManager.hideSoftInputFromWindow(\n                it.windowToken,\n                InputMethodManager.RESULT_UNCHANGED_SHOWN\n            )\n        }\n    }\n\n    override fun onDialogPositiveClick(dialog: FeedCategoriesFragment) {\n        feedFragment?.let {\n            feedCategories = EnumSet.copyOf(dialog.selectedCategories)\n            it.categories = feedCategories\n        }\n    }\n\n    private fun showToast(message: String) {\n        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()\n    }\n\n    /**\n     * Adapter that has all the information for the Fragments to feed the ViewPager2\n     */\n    internal class Adapter(fm: FragmentManager, lifecycle: Lifecycle, val context: Context) :\n        FragmentStateAdapter(fm, lifecycle) {\n        data class FragmentInfo(\n            val fragmentConstructor: () -> Fragment,\n            val title: String\n        )\n\n        private val fragmentInfo = arrayOf(\n            FragmentInfo(\n                { MainFragment() },\n                \"Main\"\n            ),\n            FragmentInfo(\n                { FeatureFlagFragment() },\n                \"Flags\"\n            ),\n            FragmentInfo(\n                { InAppMessageTesterFragment() },\n                context.getString(R.string.inappmessage_tester_tab_title)\n            ),\n            FragmentInfo(\n                { ContentCardsFragment() },\n                \"Content Cards\"\n            ),\n            FragmentInfo(\n                { PushTesterFragment() },\n                \"Push\"\n            ),\n            FragmentInfo(\n                { SettingsFragment() },\n                context.getString(R.string.settings_fragment_tab_title)\n            )\n        )\n\n        override fun getItemCount() = fragmentInfo.size\n\n        override fun createFragment(position: Int) = fragmentInfo[position].fragmentConstructor.invoke()\n\n        fun getTitle(position: Int) = fragmentInfo[position].title\n    }\n\n    companion object {\n        private var didRequestLocationPermission = false\n\n        private fun bundleToLogString(bundle: Bundle): String {\n            val bundleString = StringBuilder()\n            bundleString.append(\"Received intent with extras Bundle of size ${bundle.size()} from Braze containing [\")\n            for (key in bundle.keySet()) {\n                bundleString.append(\" '$key':'${bundle[key]}'\")\n            }\n            bundleString.append(\" ].\")\n            return bundleString.toString()\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/FeedFragmentActivity.java",
    "content": "package com.appboy.sample.activity;\n\nimport android.os.Bundle;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.appboy.sample.R;\n\npublic class FeedFragmentActivity extends AppCompatActivity {\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.com_braze_feed_activity);\n    setTitle(\"DroidGirl\");\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.java",
    "content": "package com.appboy.sample.activity;\n\nimport static com.appboy.sample.R.id.map;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.graphics.Color;\nimport android.os.Bundle;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.appboy.sample.R;\nimport com.braze.models.BrazeGeofence;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.StringUtils;\nimport com.google.android.gms.maps.CameraUpdateFactory;\nimport com.google.android.gms.maps.GoogleMap;\nimport com.google.android.gms.maps.OnMapReadyCallback;\nimport com.google.android.gms.maps.SupportMapFragment;\nimport com.google.android.gms.maps.model.CircleOptions;\nimport com.google.android.gms.maps.model.LatLng;\nimport com.google.android.gms.maps.model.MarkerOptions;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class GeofencesMapActivity extends AppCompatActivity implements OnMapReadyCallback {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(GeofencesMapActivity.class);\n  private static final String REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION = \"com.appboy.support.geofences\";\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.geofences_map);\n    SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()\n        .findFragmentById(map);\n    mapFragment.getMapAsync(this);\n  }\n\n  @Override\n  public void onMapReady(GoogleMap googleMap) {\n\n    // Note that this is for testing purposes only.  This storage location and format are not a supported API.\n    SharedPreferences registeredGeofencePrefs = getApplicationContext()\n        .getSharedPreferences(REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION, Context.MODE_PRIVATE);\n    List<BrazeGeofence> registeredGeofences = retrieveBrazeGeofencesFromLocalStorage(registeredGeofencePrefs);\n\n    int color = Color.BLUE;\n    if (registeredGeofences.size() > 0) {\n      for (BrazeGeofence registeredGeofence : registeredGeofences) {\n        googleMap.addCircle(new CircleOptions()\n            .center(new LatLng(registeredGeofence.getLatitude(), registeredGeofence.getLongitude()))\n            .radius(registeredGeofence.getRadiusMeters())\n            .strokeColor(Color.RED)\n            .fillColor(Color.argb((int) Math.round(Color.alpha(color) * .20), Color.red(color), Color.green(color), Color.blue(color))));\n        googleMap.addMarker(new MarkerOptions()\n            .position(new LatLng(registeredGeofence.getLatitude(), registeredGeofence.getLongitude()))\n            .title(\"Braze Geofence\")\n            .snippet(registeredGeofence.getLatitude() + \", \" + registeredGeofence.getLongitude()\n                + \", radius: \" + registeredGeofence.getRadiusMeters() + \"m\"));\n      }\n\n      BrazeGeofence firstGeofence = registeredGeofences.get(0);\n      googleMap.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(firstGeofence.getLatitude(), firstGeofence.getLongitude())));\n      googleMap.animateCamera(CameraUpdateFactory.zoomTo(10), null);\n    }\n  }\n\n  // Note that this is for testing purposes only.  This storage location and format are not a supported API.\n  private static List<BrazeGeofence> retrieveBrazeGeofencesFromLocalStorage(SharedPreferences sharedPreferences) {\n    List<BrazeGeofence> geofences = new ArrayList<>();\n    Map<String, ?> storedGeofences = sharedPreferences.getAll();\n    if (storedGeofences == null || storedGeofences.size() == 0) {\n      BrazeLogger.d(TAG, \"Did not find stored geofences.\");\n      return geofences;\n    }\n    Set<String> keys = storedGeofences.keySet();\n    for (String key : keys) {\n      String geofenceString = sharedPreferences.getString(key, null);\n      try {\n        if (StringUtils.isNullOrBlank(geofenceString)) {\n          BrazeLogger.w(TAG, String.format(\"Received null or blank serialized \"\n              + \" geofence string for geofence id %s from shared preferences. Not parsing.\", key));\n          continue;\n        }\n        JSONObject geofenceJson = new JSONObject(geofenceString);\n        BrazeGeofence brazeGeofence = new BrazeGeofence(geofenceJson);\n        geofences.add(brazeGeofence);\n      } catch (JSONException e) {\n        BrazeLogger.e(TAG, \"Encountered Json exception while parsing stored geofence: \" + geofenceString, e);\n      } catch (Exception e) {\n        BrazeLogger.e(TAG, \"Encountered unexpected exception while parsing stored geofence: \" + geofenceString, e);\n      }\n    }\n    return geofences;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/InAppMessageSandboxActivity.kt",
    "content": "package com.appboy.sample.activity\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport com.appboy.sample.R\nimport com.braze.enums.inappmessage.DismissType\nimport com.braze.models.inappmessage.InAppMessageModal\nimport com.braze.models.inappmessage.MessageButton\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager\nimport java.util.*\n\n/**\n * Activity whose sole purpose is to host a button that shows a basic IAM on screen.\n */\nclass InAppMessageSandboxActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_in_app_message_sandbox)\n\n        findViewById<View>(R.id.bSandboxDisplayMessage2).setOnClickListener { this.displayMessage(2) }\n        findViewById<View>(R.id.bSandboxDisplayMessage1).setOnClickListener { this.displayMessage(1) }\n        findViewById<View>(R.id.bSandboxDisplayMessage0).setOnClickListener { this.displayMessage(0) }\n        findViewById<View>(R.id.bSandboxDummyButton).setOnClickListener { Toast.makeText(this, \"dummy button pressed!\", Toast.LENGTH_SHORT).show() }\n    }\n\n    private fun displayMessage(numButtons: Int) {\n        // Create the message\n        val modal = InAppMessageModal()\n        modal.header = \"hello\"\n        modal.message = \"world\"\n        modal.remoteImageUrl = getString(R.string.appboy_image_url_1600w_500h)\n        modal.dismissType = DismissType.MANUAL\n\n        val rnd = Random()\n        val randomColor = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))\n        modal.closeButtonColor = randomColor\n\n        val button1 = MessageButton()\n        button1.text = \"Button 1\"\n        button1.borderColor = Color.RED\n        button1.backgroundColor = Color.BLUE\n\n        val button2 = MessageButton()\n        button2.text = \"Button 2\"\n        button2.borderColor = Color.CYAN\n        button2.backgroundColor = Color.BLUE\n\n        when (numButtons) {\n            2 -> modal.messageButtons = listOf(button1, button2)\n            1 -> modal.messageButtons = listOf(button1)\n        }\n        BrazeInAppMessageManager.getInstance().addInAppMessage(modal)\n        BrazeInAppMessageManager.getInstance().requestDisplayInAppMessage()\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/SettingsActivity.kt",
    "content": "package com.appboy.sample.activity\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.content.res.ResourcesCompat\nimport com.appboy.sample.R\nimport com.appboy.sample.activity.settings.SettingsFragment\n\nclass SettingsActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.settings_page)\n        val toolbar: Toolbar = findViewById(R.id.toolbar)\n        toolbar.title = getString(R.string.settings)\n        toolbar.navigationIcon = ResourcesCompat.getDrawable(resources, R.drawable.ic_back_button_droidboy, null)\n        toolbar.setNavigationOnClickListener { onBackPressed() }\n        supportFragmentManager\n            .beginTransaction()\n            .replace(R.id.settingsFragmentContainer, SettingsFragment())\n            .commit()\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/activity/settings/SettingsFragment.kt",
    "content": "package com.appboy.sample.activity.settings\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.ImageDecoder\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.SystemClock\nimport android.provider.MediaStore\nimport android.text.InputType\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.result.contract.ActivityResultContracts.RequestPermission\nimport androidx.core.content.FileProvider\nimport androidx.fragment.app.DialogFragment\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\nimport androidx.preference.PreferenceManager\nimport com.appboy.sample.BuildConfig\nimport com.appboy.sample.CustomFeedClickActionListener\nimport com.appboy.sample.DroidboyApplication\nimport com.appboy.sample.MainFragment\nimport com.appboy.sample.R\nimport com.appboy.sample.SetEnvironmentPreference\nimport com.appboy.sample.UserProfileDialog\nimport com.appboy.sample.imageloading.GlideImageLoader\nimport com.appboy.sample.logging.CustomEventDialog\nimport com.appboy.sample.logging.CustomPurchaseDialog\nimport com.appboy.sample.logging.CustomUserAttributeDialog\nimport com.appboy.sample.subscriptions.EmailSubscriptionStateDialog\nimport com.appboy.sample.subscriptions.PushSubscriptionStateDialog\nimport com.appboy.sample.util.ContentCardsTestingUtil.Companion.createRandomCards\nimport com.appboy.sample.util.EnvironmentUtils\nimport com.appboy.sample.util.LifecycleUtils\nimport com.appboy.sample.util.LogcatExportUtil.Companion.exportLogcatToFile\nimport com.appboy.sample.util.RuntimePermissionUtils\nimport com.braze.Braze\nimport com.braze.BrazeInternal\nimport com.braze.BrazeUser\nimport com.braze.Constants\nimport com.braze.images.DefaultBrazeImageLoader\nimport com.braze.images.IBrazeImageLoader\nimport com.braze.models.outgoing.AttributionData\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.braze.ui.feed.BrazeFeedManager\nimport java.io.File\n\n@SuppressLint(\"ApplySharedPref\")\nclass SettingsFragment : PreferenceFragmentCompat() {\n    private lateinit var glideImageLoader: IBrazeImageLoader\n    private lateinit var imageLoader: IBrazeImageLoader\n    private lateinit var sharedPreferences: SharedPreferences\n    private val requestPermissionLauncher =\n        registerForActivityResult(RequestPermission()) { result ->\n            Toast.makeText(\n                context,\n                \"Location permission ${if (result) \"granted\" else \"denied\"}\",\n                Toast.LENGTH_SHORT\n            ).show()\n        }\n\n    private var environmentQrPhotoUri: Uri? = null\n\n    private val cameraActivityLauncher =\n        registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->\n            if (success) {\n                environmentQrPhotoUri?.let { uri ->\n                    try {\n                        val contentResolver = requireActivity().contentResolver\n                        val bitmap: Bitmap = if (Build.VERSION.SDK_INT < 28) {\n                            @Suppress(\"DEPRECATION\")\n                            MediaStore.Images.Media.getBitmap(contentResolver, uri)\n                        } else {\n                            val source: ImageDecoder.Source = ImageDecoder.createSource(contentResolver, uri)\n                            // The copy() removes HARDWARE from the Bitmap.config, which prevents processing\n                            ImageDecoder.decodeBitmap(source).copy(Bitmap.Config.ARGB_8888, true)\n                        }\n\n                        EnvironmentUtils.analyzeBitmapForEnvironmentBarcode(\n                            this.requireActivity(),\n                            bitmap\n                        )\n                    } catch (e: Exception) {\n                        brazelog(E, e) { \"Error getting image\" }\n                    }\n                }\n            }\n        }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        setPreferencesFromResource(R.xml.preferences, rootKey)\n        val context = this.requireContext()\n\n        glideImageLoader = GlideImageLoader()\n        imageLoader = DefaultBrazeImageLoader(context)\n        sharedPreferences = context.getSharedPreferences(getString(R.string.shared_prefs_location), Context.MODE_PRIVATE)\n\n        setContentCardsPrefs(context)\n        setSdkAuthPrefs(context)\n        setNotchPrefs(context)\n        setGdprPrefs(context)\n        setNetworkPrefs(context)\n        setImageDisplayPrefs(context)\n        setSessionPrefs(context)\n        setNewsFeedPrefs(context)\n        setLocationPrefs(context)\n        setMiscellaneousPrefs(context)\n        setEnvironmentPrefs(context)\n        setAboutInfo(context)\n        setCustomLoggingSection()\n        setInAppMessagePrefs(context)\n    }\n\n    private fun setSdkAuthPrefs(context: Context) {\n        setClickPreference(\"enable_sdk_auth\") {\n            val sharedPreferencesEditor = sharedPreferences.edit()\n            sharedPreferencesEditor.putBoolean(DroidboyApplication.ENABLE_SDK_AUTH_PREF_KEY, true)\n            sharedPreferencesEditor.commit()\n            LifecycleUtils.restartApp(context)\n        }\n        setClickPreference(\"disable_sdk_auth\") {\n            val sharedPreferencesEditor = sharedPreferences.edit()\n            sharedPreferencesEditor.putBoolean(DroidboyApplication.ENABLE_SDK_AUTH_PREF_KEY, false)\n            sharedPreferencesEditor.commit()\n            LifecycleUtils.restartApp(context)\n        }\n    }\n\n    private fun setCustomLoggingSection() {\n        showDialogOnClick(\"show_user_dialog\", UserProfileDialog())\n        showDialogOnClick(\"show_user_attribute_dialog\", CustomUserAttributeDialog())\n        showDialogOnClick(\"show_custom_event_dialog\", CustomEventDialog())\n        showDialogOnClick(\"show_log_purchase_dialog\", CustomPurchaseDialog())\n        showDialogOnClick(\"show_push_subscription_state_dialog\", PushSubscriptionStateDialog())\n        showDialogOnClick(\"show_email_subscription_state_dialog\", EmailSubscriptionStateDialog())\n    }\n\n    private fun setAboutInfo(context: Context) {\n        setSummary(\"sdk_version\", Constants.BRAZE_SDK_VERSION)\n        DroidboyApplication.getApiKeyInUse(context)?.let { setSummary(\"api_key\", it) }\n        setSummary(\"push_token\", Braze.getInstance(context).registeredPushToken ?: \"No push token registered\")\n        setSummary(\"build_type\", BuildConfig.BUILD_TYPE)\n        setSummary(\"version_code\", BuildConfig.VERSION_CODE.toString())\n        setSummary(\"build_name\", BuildConfig.VERSION_NAME)\n        setSummary(\"build_name\", BuildConfig.VERSION_NAME)\n        Braze.getInstance(context).runOnUser { user -> this@SettingsFragment.setSummary(\"current_user_id\", user.userId) }\n        setSummary(\"install_time\", BuildConfig.BUILD_TIME)\n        setSummary(\"device_id\", Braze.getInstance(context).deviceId)\n    }\n\n    /**\n     * Provides the URI of a temporary file.\n     *\n     * NOTE: Calling this multiple times will return different URI's by the FileProvider, so call\n     * once and reuse the returned value\n     */\n    private fun getTmpFileUri(): Uri {\n        val context = requireContext()\n        val tmpFile = File.createTempFile(\"tmp_image_file\", \".jpg\", context.cacheDir).apply {\n            createNewFile()\n            deleteOnExit()\n        }\n\n        return FileProvider.getUriForFile(context, \"${context.packageName}.fileprovider\", tmpFile)\n    }\n\n    private fun setEnvironmentPrefs(context: Context) {\n        setClickPreference(\"environment_barcode_picture_intent_key\") {\n            // Set this here because we'll have context now\n            environmentQrPhotoUri = getTmpFileUri()\n            cameraActivityLauncher.launch(environmentQrPhotoUri)\n        }\n        setClickPreference(\"environment_reset_key\") {\n            val sharedPreferencesEditor = sharedPreferences.edit()\n            sharedPreferencesEditor.remove(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY)\n            sharedPreferencesEditor.remove(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY)\n\n            sharedPreferencesEditor.commit()\n            LifecycleUtils.restartApp(context)\n        }\n        setClickPreference(\"environment_switch_dev\") { changeEndpointToDevelopment() }\n        showDialogOnClick(\"show_set_environment_dialog\", SetEnvironmentPreference())\n    }\n\n    private fun setMiscellaneousPrefs(context: Context) {\n        setClickPreference(\"anonymous_revert\") {\n            // Note that .commit() is used here since we're restarting the process and\n            // thus need to immediately flush all shared prefs changes to disk\n            val userSharedPreferences: SharedPreferences = context.getSharedPreferences(\"com.appboy.offline.storagemap\", Context.MODE_PRIVATE)\n            userSharedPreferences\n                .edit()\n                .clear()\n                .commit()\n            val droidboySharedPrefs: SharedPreferences = context.getSharedPreferences(\"droidboy\", Context.MODE_PRIVATE)\n            droidboySharedPrefs\n                .edit()\n                .remove(MainFragment.USER_ID_KEY)\n                .commit()\n            LifecycleUtils.restartApp(context)\n        }\n        setClickPreference(\"log_attribution\") {\n            Braze.getInstance(context).runOnUser { user ->\n                val uniqueInt = SystemClock.currentThreadTimeMillis() % 1000\n                user.setAttributionData(\n                    AttributionData(\n                        \"network_val_$uniqueInt\",\n                        \"campaign_val_$uniqueInt\",\n                        \"adgroup_val_$uniqueInt\",\n                        \"creative_val_$uniqueInt\"\n                    )\n                )\n                showToast(\"Attribution data sent to server\")\n            }\n        }\n        setClickPreference(\"logcat_export_file_key\") {\n            val logcatFileUri = exportLogcatToFile(context)\n            val shareIntent = Intent(Intent.ACTION_SEND)\n            shareIntent.type = \"text/plain\"\n            shareIntent.putExtra(Intent.EXTRA_STREAM, logcatFileUri)\n\n            // Grant temporary read permission to the content URI\n            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n            startActivity(Intent.createChooser(shareIntent, \"Export logcat as a big text file\"))\n        }\n    }\n\n    private fun setLocationPrefs(context: Context) {\n        setClickPreference(\"set_manual_location\") {\n            Braze.getInstance(context).runOnUser { user ->\n                user.setLastKnownLocation(1.0, 2.0, 3.0, 4.0)\n                showToast(\"Manually set location to latitude 1.0d, longitude 2.0d, altitude 3.0m, accuracy 4.0m.\")\n            }\n        }\n        setClickPreference(\"location_runtime_permission_dialog\") {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                RuntimePermissionUtils.requestLocationPermission(\n                    activity as Activity,\n                    Manifest.permission.ACCESS_FINE_LOCATION,\n                    requestPermissionLauncher\n                )\n            } else {\n                showToast(\"Below Android M there is no need to check for runtime permissions.\")\n            }\n        }\n    }\n\n    private fun setNewsFeedPrefs(context: Context) {\n        setSwitchPreference(\"sort_feed\") { newValue: Boolean ->\n            val sharedPref: SharedPreferences = context.getSharedPreferences(getString(R.string.feed), Context.MODE_PRIVATE)\n            val editor = sharedPref.edit()\n            editor.putBoolean(getString(R.string.sort_feed), newValue)\n            editor.apply()\n        }\n        setSwitchPreference(\"set_custom_news_feed_card_click_action_listener\") { newValue: Boolean ->\n            BrazeFeedManager.getInstance().feedCardClickActionListener = if (newValue) CustomFeedClickActionListener() else null\n        }\n    }\n\n    private fun setInAppMessagePrefs(context: Context) {\n        setEditTextPreference(\"min_trigger_interval\", true) { newValue: String ->\n            if (newValue.isEmpty()) {\n                showToast(\"Clearing setting. Restart for new value to take effect\")\n                sharedPreferences.edit().remove(\"min_trigger_interval\").apply()\n                return@setEditTextPreference\n            }\n\n            newValue.toIntOrNull() ?: run {\n                Toast.makeText(context, \"Interval must be only digits\", Toast.LENGTH_LONG).show()\n                sharedPreferences.edit().remove(\"min_trigger_interval\").apply()\n                return@setEditTextPreference\n            }\n\n            sharedPreferences.edit().putString(\"min_trigger_interval\", newValue).apply()\n            Toast.makeText(context, \"Restart for new value to take effect\", Toast.LENGTH_LONG).show()\n        }\n    }\n\n    private fun setSessionPrefs(context: Context) {\n        setClickPreference(\"open_session\") { Braze.getInstance(context).openSession(this.activity) }\n        setClickPreference(\"close_session\") {\n            Braze.getInstance(context).closeSession(this.activity)\n            showToast(getString(R.string.close_session_toast))\n        }\n    }\n\n    private fun setImageDisplayPrefs(context: Context) {\n        setClickPreference(\"glide_image_loader_enable_setting_key\") {\n            Braze.getInstance(context).imageLoader = glideImageLoader\n            showToast(\"Glide enabled\")\n        }\n        setClickPreference(\"glide_image_loader_disable_setting_key\") {\n            Braze.getInstance(context).imageLoader = imageLoader\n            showToast(\"Glide disabled. Default Image loader in use.\")\n        }\n    }\n\n    private fun setNetworkPrefs(context: Context) {\n        setClickPreference(\"disable_outbound_network_requests\") {\n            Braze.outboundNetworkRequestsOffline = true\n            showToast(getString(R.string.disabled_outbound_network_requests_toast))\n        }\n        setClickPreference(\"enable_outbound_network_requests\") {\n            Braze.outboundNetworkRequestsOffline = false\n            showToast(getString(R.string.enabled_outbound_network_requests_toast))\n        }\n        setClickPreference(\"data_flush\") {\n            Braze.getInstance(context).requestImmediateDataFlush()\n            showToast(getString(R.string.data_flush_toast))\n        }\n    }\n\n    private fun setGdprPrefs(context: Context) {\n        setClickPreference(\"wipe_data_preference_key\") { Braze.wipeData(context) }\n        setClickPreference(\"enable_sdk_key\") { Braze.enableSdk(context) }\n        setClickPreference(\"disable_sdk_key\") { Braze.disableSdk(context) }\n    }\n\n    private fun setContentCardsPrefs(context: Context) {\n        setClickPreference(\"content_card_dismiss_all_cards_setting_key\") {\n            val cachedContentCards = Braze.getInstance(context).getCachedContentCards()\n            if (cachedContentCards != null) {\n                for (card in cachedContentCards) {\n                    card.isDismissed = true\n                }\n            }\n        }\n        setClickPreference(\"content_card_populate_random_cards_setting_key\") {\n            val randomCards = createRandomCards(context, 5)\n            Braze.getInstance(context).currentUser?.userId?.let { userId ->\n                randomCards.forEach { card ->\n                    BrazeInternal.addSerializedContentCardToStorage(context, card.forJsonPut().toString(), userId)\n                }\n            }\n        }\n    }\n\n    private fun setNotchPrefs(context: Context) {\n        setSwitchPreference(\"display_in_full_cutout_setting_key\") { newValue: Boolean ->\n            // Restart the app to force onCreate() to re-run\n            // Note that an app restart won't commit prefs changes so we have to do it manually\n            PreferenceManager.getDefaultSharedPreferences(context).edit()\n                .putBoolean(\"display_in_full_cutout_setting_key\", newValue)\n                .commit()\n            LifecycleUtils.restartApp(context)\n        }\n        setSwitchPreference(\"display_no_limits_setting_key\") { newValue: Boolean ->\n            // Restart the app to force onCreate() to re-run\n            // Note that an app restart won't commit prefs changes so we have to do it manually\n            PreferenceManager.getDefaultSharedPreferences(context).edit()\n                .putBoolean(\"display_no_limits_setting_key\", newValue)\n                .commit()\n            LifecycleUtils.restartApp(context)\n        }\n    }\n\n    private fun showToast(message: String) {\n        Handler(this.requireActivity().mainLooper).post {\n            Toast.makeText(this.requireContext(), message, Toast.LENGTH_LONG).show()\n        }\n    }\n\n    private fun changeEndpointToDevelopment() {\n        Braze.wipeData(requireContext())\n        Braze.enableSdk(requireContext())\n        val sharedPreferencesEditor: SharedPreferences.Editor = sharedPreferences.edit()\n        if (Constants.isAmazonDevice) {\n            sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY, DEV_FIREOS_DROIDBOY_API_KEY)\n        } else {\n            sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY, DEV_DROIDBOY_API_KEY)\n        }\n        sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY, DEV_SDK_ENDPOINT)\n        sharedPreferencesEditor.commit()\n        LifecycleUtils.restartApp(this.requireContext())\n    }\n\n    private fun showDialogOnClick(key: String, dialog: DialogFragment) {\n        setClickPreference(key) {\n            dialog.show(childFragmentManager, \"\")\n        }\n    }\n\n    companion object {\n        private const val DEV_DROIDBOY_API_KEY = \"da8f263e-1483-4e9f-ac0c-7b40030c8f40\"\n        private const val DEV_FIREOS_DROIDBOY_API_KEY = \"ecb81855-149f-465c-bab0-0254d6512133\"\n        private const val DEV_SDK_ENDPOINT = \"https://elsa.braze.com/\"\n\n        /**\n         * Extension function for preferences that are only clicked\n         */\n        fun PreferenceFragmentCompat.setClickPreference(key: String, block: () -> Unit) {\n            this.findPreference<Preference>(key)?.setOnPreferenceClickListener {\n                block.invoke()\n                return@setOnPreferenceClickListener true\n            }\n        }\n\n        fun PreferenceFragmentCompat.setSwitchPreference(key: String, block: (newValue: Boolean) -> Unit) {\n            this.findPreference<Preference>(key)?.setOnPreferenceChangeListener { _, newValue ->\n                block.invoke(newValue as @kotlin.ParameterName(name = \"newValue\") Boolean)\n                return@setOnPreferenceChangeListener true\n            }\n        }\n\n        fun PreferenceFragmentCompat.setEditTextPreference(key: String, numberOnly: Boolean = false, block: (newValue: String) -> Unit) {\n            this.findPreference<Preference>(key)?.setOnPreferenceChangeListener { _, newValue ->\n                block.invoke(newValue as @kotlin.ParameterName(name = \"newValue\") String)\n                return@setOnPreferenceChangeListener true\n            }\n\n            if (numberOnly) {\n                (this.findPreference<Preference>(key) as EditTextPreference?)?.setOnBindEditTextListener {\n                    it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED\n                }\n            }\n        }\n\n        fun PreferenceFragmentCompat.setSummary(key: String, summary: String) {\n            this.findPreference<Preference>(key)?.summary = summary\n        }\n\n        fun Braze.runOnUser(block: (user: BrazeUser) -> Unit) {\n            this.getCurrentUser { user ->\n                block(user)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/dialog/CustomDialogBase.kt",
    "content": "package com.appboy.sample.dialog\n\nimport android.os.Bundle\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport androidx.fragment.app.DialogFragment\nimport com.appboy.sample.R\n\n/**\n * Requires R.layout.dialog_footer_navigation in the dialog to work properly.\n */\nabstract class CustomDialogBase : DialogFragment() {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        view.findViewById<Button>(R.id.bDialogNegative).setOnClickListener { onExitButtonPressed(false) }\n        view.findViewById<Button>(R.id.bDialogPositive).setOnClickListener { onExitButtonPressed(true) }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        val width = (resources.displayMetrics.widthPixels * 0.95).toInt()\n        dialog?.window?.setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)\n    }\n\n    abstract fun onExitButtonPressed(isPositive: Boolean)\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/featureflag/controller/FeatureFlagAdapter.kt",
    "content": "package com.appboy.sample.featureflag.controller\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.RecyclerView\nimport com.appboy.sample.R\nimport com.braze.models.FeatureFlag\nimport com.braze.support.getPrettyPrintedString\n\nclass FeatureFlagAdapter(\n    private val featureFlags: MutableList<FeatureFlag>\n) : RecyclerView.Adapter<FeatureFlagAdapter.ViewHolder>() {\n    /**\n     * Provide a reference to the type of views that you are using\n     * (custom ViewHolder)\n     */\n    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {\n        private var areDetailsVisible = false\n        val tvIdentifier: TextView\n        val tvNumProperties: TextView\n        val tvEnabled: TextView\n        val tvPropertyDetails: TextView\n\n        init {\n            // Define click listener for the ViewHolder's View\n            tvIdentifier = view.findViewById(R.id.tvFlagId)\n            tvNumProperties = view.findViewById(R.id.tvFlagNumProperties)\n            tvEnabled = view.findViewById(R.id.tvFlagEnabled)\n            tvPropertyDetails = view.findViewById(R.id.tvFlagProperties)\n\n            itemView.setOnClickListener {\n                areDetailsVisible = !areDetailsVisible\n                tvPropertyDetails.visibility = if (areDetailsVisible) View.VISIBLE else View.GONE\n            }\n        }\n    }\n\n    // Create new views (invoked by the layout manager)\n    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {\n        // Create a new view, which defines the UI of the list item\n        val view = LayoutInflater.from(viewGroup.context)\n            .inflate(R.layout.feature_flag_overview_item, viewGroup, false)\n\n        return ViewHolder(view)\n    }\n\n    // Replace the contents of a view (invoked by the layout manager)\n    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {\n        // Get element from your dataset at this position and replace the\n        // contents of the view with that element\n        val featureFlag = featureFlags[position]\n        viewHolder.run {\n            tvIdentifier.text = featureFlag.id\n            tvEnabled.text = if (featureFlag.enabled) \"ON\" else \"OFF\"\n            tvNumProperties.text = featureFlag.properties.length().toString()\n            tvPropertyDetails.text = featureFlag.properties.getPrettyPrintedString()\n        }\n    }\n\n    // Return the size of your dataset (invoked by the layout manager)\n    override fun getItemCount() = featureFlags.size\n\n    fun replaceFeatureFlags(newFlagData: List<FeatureFlag>) {\n        val diffCallback = DiffCallback(featureFlags, newFlagData)\n        val diffResult = DiffUtil.calculateDiff(diffCallback)\n        featureFlags.clear()\n        featureFlags.addAll(newFlagData)\n        diffResult.dispatchUpdatesTo(this)\n    }\n\n    private class DiffCallback(\n        private val oldFlags: List<FeatureFlag>,\n        private val newFlags: List<FeatureFlag>\n    ) : DiffUtil.Callback() {\n        override fun getOldListSize() =\n            oldFlags.size\n\n        override fun getNewListSize() =\n            newFlags.size\n\n        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =\n            doItemsShareIds(oldItemPosition, newItemPosition)\n\n        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =\n            oldFlags[oldItemPosition].forJsonPut() == newFlags[newItemPosition].forJsonPut()\n\n        private fun doItemsShareIds(oldItemPosition: Int, newItemPosition: Int) =\n            oldFlags[oldItemPosition].id == newFlags[newItemPosition].id\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/featureflag/view/FeatureFlagFragment.kt",
    "content": "package com.appboy.sample.featureflag.view\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout\nimport com.appboy.sample.R\nimport com.appboy.sample.featureflag.controller.FeatureFlagAdapter\nimport com.braze.Braze\nimport com.braze.coroutine.BrazeCoroutineScope\nimport com.braze.events.FeatureFlagsUpdatedEvent\nimport com.braze.events.IEventSubscriber\nimport com.braze.models.FeatureFlag\n\nclass FeatureFlagFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {\n    private lateinit var dataAdapter: FeatureFlagAdapter\n    private lateinit var recyclerView: RecyclerView\n    private lateinit var swipeRefreshLayout: SwipeRefreshLayout\n\n    private val updateListener = IEventSubscriber<FeatureFlagsUpdatedEvent> {\n        handleFeatureFlagUpdate(it.featureFlags)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        // Inflate the layout for this fragment\n        val rootView = inflater.inflate(R.layout.feature_flag_fragment, container, false)\n        recyclerView = rootView.findViewById(R.id.flag_overview_recycler_view)\n        recyclerView.setHasFixedSize(true)\n        swipeRefreshLayout = rootView.findViewById(R.id.feature_flag_swipe_container)\n        swipeRefreshLayout.setOnRefreshListener(this)\n        swipeRefreshLayout.setColorSchemeColors(Color.CYAN, Color.GREEN)\n        return rootView\n    }\n\n    override fun onViewStateRestored(savedInstanceState: Bundle?) {\n        super.onViewStateRestored(savedInstanceState)\n        dataAdapter = FeatureFlagAdapter(mutableListOf())\n        recyclerView.adapter = dataAdapter\n        recyclerView.layoutManager = LinearLayoutManager(activity)\n        val itemDecoration: RecyclerView.ItemDecoration =\n            DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)\n        recyclerView.addItemDecoration(itemDecoration)\n        requestFeatureFlagUpdate()\n    }\n\n    private fun requestFeatureFlagUpdate() {\n        // Listen for new Feature Flag updates after retrieving the current set\n        Braze.getInstance(requireContext()).let {\n            handleFeatureFlagUpdate(it.getAllFeatureFlags())\n            it.subscribeToFeatureFlagsUpdates(updateListener)\n            it.refreshFeatureFlags()\n        }\n    }\n\n    private fun handleFeatureFlagUpdate(flags: List<FeatureFlag>) {\n        if (this::dataAdapter.isInitialized) {\n            recyclerView.post {\n                dataAdapter.replaceFeatureFlags(flags)\n            }\n        }\n    }\n\n    override fun onRefresh() {\n        requestFeatureFlagUpdate()\n        BrazeCoroutineScope.launchDelayed(AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS) { swipeRefreshLayout.isRefreshing = false }\n    }\n\n    companion object {\n        private const val AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS = 1500L\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/imageloading/GlideImageLoader.kt",
    "content": "package com.appboy.sample.imageloading\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.widget.ImageView\nimport com.braze.models.cards.Card\nimport com.braze.enums.BrazeViewBounds\nimport com.braze.images.IBrazeImageLoader\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.brazelog\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.request.RequestOptions\nimport com.bumptech.glide.signature.ObjectKey\n\nclass GlideImageLoader : IBrazeImageLoader {\n    private val requestOptions: RequestOptions\n        get() {\n            // Create default options where images are only cached for 1 minute\n            return RequestOptions()\n                .onlyRetrieveFromCache(isOffline)\n                .signature(ObjectKey(System.currentTimeMillis() / (1000 * 60 * 1)))\n        }\n\n    private var isOffline = false\n\n    override fun renderUrlIntoCardView(\n        context: Context,\n        card: Card,\n        imageUrl: String,\n        imageView: ImageView,\n        viewBounds: BrazeViewBounds?\n    ) {\n        renderUrlIntoView(context, imageUrl, imageView)\n    }\n\n    override fun renderUrlIntoInAppMessageView(\n        context: Context,\n        inAppMessage: IInAppMessage,\n        imageUrl: String,\n        imageView: ImageView,\n        viewBounds: BrazeViewBounds?\n    ) {\n        renderUrlIntoView(context, imageUrl, imageView)\n    }\n\n    override fun getPushBitmapFromUrl(\n        context: Context,\n        extras: Bundle?,\n        imageUrl: String,\n        viewBounds: BrazeViewBounds?\n    ): Bitmap? = getBitmapFromUrl(context, imageUrl)\n\n    override fun getInAppMessageBitmapFromUrl(\n        context: Context,\n        inAppMessage: IInAppMessage,\n        imageUrl: String,\n        viewBounds: BrazeViewBounds?\n    ): Bitmap? = getBitmapFromUrl(context, imageUrl)\n\n    private fun renderUrlIntoView(\n        context: Context,\n        imageUrl: String,\n        imageView: ImageView\n    ) {\n        Glide.with(context)\n            .load(imageUrl)\n            .apply(requestOptions)\n            .into(imageView)\n    }\n\n    private fun getBitmapFromUrl(context: Context, imageUrl: String): Bitmap? {\n        try {\n            return Glide.with(context)\n                .asBitmap()\n                .apply(requestOptions)\n                .load(imageUrl).submit().get()\n        } catch (e: Exception) {\n            brazelog(E, e) { \"Failed to retrieve bitmap at url: $imageUrl\" }\n        }\n        return null\n    }\n\n    override fun setOffline(isOffline: Boolean) {\n        // If the loader is offline, then we should only be retrieving from the cache\n        this.isOffline = isOffline\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/logging/CustomEventDialog.java",
    "content": "package com.appboy.sample.logging;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.R;\nimport com.braze.Braze;\nimport com.braze.models.outgoing.BrazeProperties;\n\npublic class CustomEventDialog extends CustomLogger {\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    return inflater.inflate(R.layout.custom_event, container, false);\n  }\n\n  @Override\n  protected void customLog(String name, BrazeProperties properties) {\n    Braze.getInstance(getContext()).logCustomEvent(name, properties);\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/logging/CustomLogger.java",
    "content": "package com.appboy.sample.logging;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.LinearLayout;\nimport android.widget.Spinner;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.R;\nimport com.appboy.sample.dialog.CustomDialogBase;\nimport com.appboy.sample.util.ButtonUtils;\nimport com.braze.Braze;\nimport com.braze.models.outgoing.BrazeProperties;\nimport com.braze.support.StringUtils;\n\npublic abstract class CustomLogger extends CustomDialogBase {\n  private Context mContext;\n  private EditText mName;\n  private PropertyManager mPropertyManager;\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n    mContext = view.getContext();\n    mName = view.findViewById(R.id.custom_name);\n    EditText propertyKey = view.findViewById(R.id.property_key);\n    EditText propertyValue = view.findViewById(R.id.property_value);\n    Spinner typeSpinner = view.findViewById(R.id.property_type_spinner);\n    Button addProperty = view.findViewById(R.id.add_property_button);\n    LinearLayout propertyLayout = view.findViewById(R.id.property_linear_layout);\n\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_name_button, mName, \"football\");\n\n    mPropertyManager = new PropertyManager(mContext, propertyLayout, propertyKey, propertyValue, typeSpinner, addProperty);\n  }\n\n  @Override\n  public void onExitButtonPressed(boolean isImmediateFlushRequired) {\n    String customName = mName.getText().toString();\n    if (!StringUtils.isNullOrBlank(customName)) {\n      customLog(customName, mPropertyManager.getBrazeProperties());\n      notifyResult(customName);\n    } else {\n      Toast.makeText(mContext, \"Must input a name\", Toast.LENGTH_LONG).show();\n    }\n\n    // Flushing manually is not recommended in almost all production situations as\n    // Braze automatically flushes data to its servers periodically. This call\n    // is solely for testing purposes.\n    if (isImmediateFlushRequired) {\n      Braze.getInstance(mContext).requestImmediateDataFlush();\n    }\n    this.dismiss();\n  }\n\n  private void notifyResult(String input) {\n    Toast.makeText(mContext, \"Successfully submitted \" + input + \".\", Toast.LENGTH_LONG).show();\n  }\n\n  protected abstract void customLog(String name, BrazeProperties properties);\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/logging/CustomPurchaseDialog.java",
    "content": "package com.appboy.sample.logging;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.EditText;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.R;\nimport com.appboy.sample.util.ButtonUtils;\nimport com.braze.Braze;\nimport com.braze.models.outgoing.BrazeProperties;\nimport com.braze.support.StringUtils;\n\nimport java.math.BigDecimal;\n\npublic class CustomPurchaseDialog extends CustomLogger {\n  private static final String DEFAULT_CURRENCY_CODE = \"USD\";\n  private static final String DEFAULT_PRICE = \"10.0\";\n  private EditText mCustomPurchaseQuantity;\n  private EditText mCustomPurchaseCurrencyCodeName;\n  private EditText mCustomPurchasePrice;\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    final View view = inflater.inflate(R.layout.custom_purchase, container, false);\n    mCustomPurchaseQuantity = view.findViewById(R.id.purchase_qty);\n    mCustomPurchaseCurrencyCodeName = view.findViewById(R.id.custom_purchase_currency_code);\n    mCustomPurchasePrice = view.findViewById(R.id.custom_purchase_price_code);\n\n    ButtonUtils.setUpPopulateButton(view, R.id.purchase_qty_button, mCustomPurchaseQuantity, \"5\");\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_purchase_currency_code_button, mCustomPurchaseCurrencyCodeName, \"JPY\");\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_purchase_price_button, mCustomPurchasePrice, \"5.0\");\n    return view;\n  }\n\n  @Override\n  protected void customLog(String name, BrazeProperties properties) {\n    String currencyCode = mCustomPurchaseCurrencyCodeName.getText().toString();\n    String quantity = mCustomPurchaseQuantity.getText().toString();\n    String price = mCustomPurchasePrice.getText().toString();\n\n    if (StringUtils.isNullOrBlank(currencyCode)) {\n      currencyCode = DEFAULT_CURRENCY_CODE;\n    }\n    if (StringUtils.isNullOrBlank(price)) {\n      price = DEFAULT_PRICE;\n    }\n    if (StringUtils.isNullOrBlank(quantity)) {\n      Braze.getInstance(getContext()).logPurchase(name, currencyCode, new BigDecimal(price), properties);\n      return;\n    }\n    Braze.getInstance(getContext()).logPurchase(name, currencyCode, new BigDecimal(price), Integer.parseInt(quantity), properties);\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/logging/CustomUserAttributeDialog.java",
    "content": "package com.appboy.sample.logging;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.EditText;\nimport android.widget.RadioGroup;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.R;\nimport com.appboy.sample.dialog.CustomDialogBase;\nimport com.appboy.sample.util.ButtonUtils;\nimport com.braze.Braze;\nimport com.braze.BrazeUser;\nimport com.braze.support.StringUtils;\n\npublic class CustomUserAttributeDialog extends CustomDialogBase {\n  private static final String EMPTY_STRING = \"\";\n  private static final String DEFAULT_NUMBER_VALUE = \"5\";\n  private EditText mCustomAttributeKey;\n  private EditText mCustomAttributeValue;\n  private EditText mCustomAttributeNumberKey;\n  private EditText mCustomAttributeNumberValue;\n  private EditText mCustomAttributeIncrementKey;\n  private EditText mCustomAttributeIncrementValue;\n  private EditText mCustomAttributeUnsetKey;\n  private EditText mCustomAttributeArrayKey;\n  private EditText mCustomAttributeArrayValue;\n  private RadioGroup mCustomAttributeArrayChoices;\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    return inflater.inflate(R.layout.custom_attribute, container, false);\n  }\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n\n    mCustomAttributeKey = view.findViewById(R.id.custom_attribute_key);\n    mCustomAttributeValue = view.findViewById(R.id.custom_attribute_value);\n    mCustomAttributeNumberKey = view.findViewById(R.id.custom_number_attribute_key);\n    mCustomAttributeNumberValue = view.findViewById(R.id.custom_number_attribute_value);\n    mCustomAttributeIncrementKey = view.findViewById(R.id.custom_attribute_increment_key);\n    mCustomAttributeIncrementValue = view.findViewById(R.id.custom_attribute_increment_value);\n    mCustomAttributeUnsetKey = view.findViewById(R.id.custom_attribute_unset_key);\n    mCustomAttributeArrayKey = view.findViewById(R.id.custom_attribute_array_key);\n    mCustomAttributeArrayValue = view.findViewById(R.id.custom_attribute_array_value);\n    mCustomAttributeArrayChoices = view.findViewById(R.id.custom_attribute_array_radio);\n\n    mCustomAttributeArrayChoices.check(R.id.custom_attribute_array_set);\n\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_attribute_button, mCustomAttributeKey, \"color\", mCustomAttributeValue, \"green\");\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_number_attribute_button, mCustomAttributeNumberKey, \"leagues\", mCustomAttributeNumberValue, DEFAULT_NUMBER_VALUE);\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_attribute_increment_button, mCustomAttributeIncrementKey, \"height\", mCustomAttributeIncrementValue, \"10\");\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_attribute_unset_button, mCustomAttributeUnsetKey, \"color\");\n    ButtonUtils.setUpPopulateButton(view, R.id.custom_attribute_array_button, mCustomAttributeArrayKey, \"toys\", mCustomAttributeArrayValue, \"doll\");\n  }\n\n  @Override\n  public void onExitButtonPressed(boolean isImmediateFlushRequired) {\n    String customAttributeKeyName = mCustomAttributeKey.getText().toString();\n    String customAttributeValueName = mCustomAttributeValue.getText().toString();\n    String customNumberAttributeKeyName = mCustomAttributeNumberKey.getText().toString();\n    // Note, this is a number in string form, we'll have to parse it later\n    String customNumberAttributeValueName = mCustomAttributeNumberValue.getText().toString();\n    String customAttributeIncrementKeyName = mCustomAttributeIncrementKey.getText().toString();\n    String customAttributeIncrementValueName = mCustomAttributeIncrementValue.getText().toString();\n    String customAttributeUnsetKeyName = mCustomAttributeUnsetKey.getText().toString();\n    String customAttributeArrayKey = mCustomAttributeArrayKey.getText().toString();\n    String customAttributeArrayValue = mCustomAttributeArrayValue.getText().toString();\n    int attributeArrayResourceId = mCustomAttributeArrayChoices.getCheckedRadioButtonId();\n\n    if (!StringUtils.isNullOrBlank(customAttributeKeyName)) {\n      if (StringUtils.isNullOrBlank(customAttributeValueName)) {\n        customAttributeValueName = EMPTY_STRING;\n      }\n      BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n      notifyResult(brazeUser.setCustomUserAttribute(customAttributeKeyName, customAttributeValueName),\n          \"set user attribute! key=\" + customAttributeKeyName + \", value=\" + customAttributeValueName);\n    }\n    if (!StringUtils.isNullOrBlank(customNumberAttributeKeyName)) {\n      if (StringUtils.isNullOrBlank(customNumberAttributeValueName)) {\n        customNumberAttributeValueName = DEFAULT_NUMBER_VALUE;\n      }\n      // Convert the value into an integer\n      int numberValue = stringToInteger(customNumberAttributeValueName);\n\n      BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n      notifyResult(brazeUser.setCustomUserAttribute(customNumberAttributeKeyName, numberValue),\n          \"set user number attribute! key=\" + customAttributeKeyName + \", value=\" + numberValue);\n    }\n    if (!StringUtils.isNullOrBlank(customAttributeIncrementKeyName)) {\n      BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n      if (!StringUtils.isNullOrBlank(customAttributeIncrementValueName)) {\n        int incrementValue = Integer.parseInt(customAttributeIncrementValueName);\n        notifyResult(brazeUser.incrementCustomUserAttribute(customAttributeIncrementKeyName, incrementValue),\n            \"Increment user attribute! key=\" + customAttributeIncrementKeyName + \", value=\" + customAttributeIncrementValueName);\n      } else {\n        notifyResult(brazeUser.incrementCustomUserAttribute(customAttributeIncrementKeyName),\n            \"Increment user attribute! key=\" + customAttributeIncrementKeyName + \", value=1\");\n      }\n    }\n    if (!StringUtils.isNullOrBlank(customAttributeUnsetKeyName)) {\n      BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n      notifyResult(brazeUser.unsetCustomUserAttribute(customAttributeUnsetKeyName), \"Unset user attribute! key=\" + customAttributeUnsetKeyName);\n    }\n    if (!StringUtils.isNullOrBlank(customAttributeArrayKey)) {\n      if (StringUtils.isNullOrBlank(customAttributeArrayValue)) {\n        customAttributeArrayValue = EMPTY_STRING;\n      }\n      BrazeUser brazeUser = Braze.getInstance(getContext()).getCurrentUser();\n      switch (attributeArrayResourceId) {\n        case R.id.custom_attribute_array_set:\n          String[] attributeArray = {customAttributeArrayValue};\n          notifyResult(brazeUser.setCustomAttributeArray(customAttributeArrayKey, attributeArray),\n              \"setCustomAttributeArray! Setting new array key=\" + customAttributeArrayKey + \", values={\" + customAttributeArrayValue + \"}\");\n          break;\n        case R.id.custom_attribute_array_add:\n          notifyResult(brazeUser.addToCustomAttributeArray(customAttributeArrayKey, customAttributeArrayValue),\n              \"addToCustomAttributeArray! Adding value=\" + customAttributeArrayValue + \" to array with key=\" + customAttributeArrayKey + \".\");\n          break;\n        case R.id.custom_attribute_array_remove:\n          notifyResult(brazeUser.removeFromCustomAttributeArray(customAttributeArrayKey, customAttributeArrayValue),\n              \"removeFromCustomAttributeArray! Will remove value=\" + customAttributeArrayValue + \" from array with key=\" + customAttributeArrayKey + \".\");\n          break;\n        default:\n          notifyResult(false, \"Error parsing attribute array radio button: \" + attributeArrayResourceId);\n      }\n    }\n\n    // Flushing manually is not recommended in almost all production situations as\n    // Braze automatically flushes data to its servers periodically. This call\n    // is solely for testing purposes.\n    if (isImmediateFlushRequired) {\n      Braze.getInstance(getContext()).requestImmediateDataFlush();\n    }\n    this.dismiss();\n  }\n\n  private void notifyResult(boolean result, String input) {\n    if (result) {\n      Toast.makeText(getContext(), \"Successfully logged \" + input + \".\", Toast.LENGTH_LONG).show();\n    } else {\n      Toast.makeText(getContext(), \"Failed to log \" + input + \".\", Toast.LENGTH_LONG).show();\n    }\n  }\n\n  /**\n   * Parses a string into an integer. Defaults to 1 if the string cannot be parsed into an integer.\n   */\n  private int stringToInteger(String num) {\n    try {\n      return Integer.parseInt(num);\n    } catch (NumberFormatException e) {\n      return 5;\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/logging/PropertyManager.java",
    "content": "package com.appboy.sample.logging;\n\nimport android.app.DatePickerDialog;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.LinearLayout;\nimport android.widget.Spinner;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.appboy.sample.R;\nimport com.braze.models.outgoing.BrazeProperties;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\npublic class PropertyManager implements AdapterView.OnItemSelectedListener {\n  private static final String[] propertyTypes = {\"integer\", \"double\", \"string\", \"boolean\", \"date\", \"long\"};\n  private final Map<String, Object> mProperties = new HashMap<>();\n  private final List<String> mKeys = new ArrayList<>();\n  private int selectedPropertyType;\n  private Date lastDatePicked;\n  private final LinearLayout mLinearLayout;\n  private final EditText mPropertyKey;\n  private final EditText mPropertyValue;\n  private final Context mContext;\n\n  public PropertyManager(Context context, LinearLayout linearLayout, EditText propertyKey, EditText propertyValue, Spinner propertyTypeSpinner, Button addPropertyButton) {\n    mContext = context;\n    mLinearLayout = linearLayout;\n    mPropertyKey = propertyKey;\n    mPropertyValue = propertyValue;\n    ArrayAdapter<String> adapter = new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_dropdown_item, propertyTypes);\n    propertyTypeSpinner.setAdapter(adapter);\n    propertyTypeSpinner.setOnItemSelectedListener(this);\n    addPropertyButton.setOnClickListener(view -> {\n      String key = mPropertyKey.getText().toString();\n      Object value;\n      switch (selectedPropertyType) {\n        case 0:\n          value = getIntegerProperty();\n          break;\n        case 1:\n          value = getDoubleProperty();\n          break;\n        case 2:\n          value = getStringProperty();\n          break;\n        case 3:\n          value = getBooleanProperty();\n          break;\n        case 4:\n          value = lastDatePicked;\n          break;\n        case 5:\n          value = getLongProperty();\n          break;\n        default:\n          value = null;\n          break;\n      }\n      if (value != null) {\n        mPropertyKey.setText(\"\");\n        mPropertyValue.setText(\"\");\n        addPropertyView(key, value);\n      }\n    });\n  }\n\n  public BrazeProperties getBrazeProperties() {\n    BrazeProperties brazeProperties = new BrazeProperties();\n    for (String key : mKeys) {\n      Object value = mProperties.get(key);\n      if (value instanceof Integer) {\n        brazeProperties.addProperty(key, (int) value);\n      } else if (value instanceof Double) {\n        brazeProperties.addProperty(key, (double) value);\n      } else if (value instanceof Boolean) {\n        brazeProperties.addProperty(key, (boolean) value);\n      } else if (value instanceof String) {\n        brazeProperties.addProperty(key, (String) value);\n      } else if (value instanceof Date) {\n        brazeProperties.addProperty(key, (Date) value);\n      } else if (value instanceof Long) {\n        brazeProperties.addProperty(key, (long) value);\n      } else {\n        BrazeLogger.w(this.getClass().toString(), \"invalid property type\");\n      }\n    }\n    return brazeProperties;\n  }\n\n  private void addPropertyView(String key, Object value) {\n    mKeys.add(key);\n    mProperties.put(key, value);\n    View view = View.inflate(mContext, R.layout.property_list_item, null);\n    TextView keyTextView = view.findViewById(R.id.key_text_view);\n    TextView valTextView = view.findViewById(R.id.value_text_view);\n    keyTextView.setText(key);\n    if (value instanceof Date) {\n      Calendar calendar = Calendar.getInstance();\n      calendar.setTime((Date) value);\n      valTextView.setText(String.format(Locale.getDefault(), \"%d/%d/%d\",\n              calendar.get(Calendar.MONTH) + 1,\n              calendar.get(Calendar.DAY_OF_MONTH),\n              calendar.get(Calendar.YEAR)));\n    } else {\n      valTextView.setText(value.toString());\n    }\n    mLinearLayout.addView(view);\n  }\n\n  private Object getIntegerProperty() {\n    try {\n      return Integer.parseInt(mPropertyValue.getText().toString());\n    } catch (NumberFormatException nfe) {\n      Toast.makeText(mContext, \"Make sure the value is an integer\", Toast.LENGTH_LONG).show();\n      return null;\n    }\n  }\n\n  private Object getLongProperty() {\n    try {\n      return Long.parseLong(mPropertyValue.getText().toString());\n    } catch (NumberFormatException nfe) {\n      Toast.makeText(mContext, \"Make sure the value is a long\", Toast.LENGTH_LONG).show();\n      return null;\n    }\n  }\n\n  private Object getDoubleProperty() {\n    try {\n      return Double.parseDouble(mPropertyValue.getText().toString());\n    } catch (NumberFormatException nfe) {\n      Toast.makeText(mContext, \"Make sure the value is a double\", Toast.LENGTH_LONG).show();\n      return null;\n    }\n  }\n\n  private Object getStringProperty() {\n    String value = mPropertyValue.getText().toString();\n    if (StringUtils.isNullOrBlank(value)) {\n      Toast.makeText(mContext, \"value should not be blank\", Toast.LENGTH_LONG).show();\n      return null;\n    } else {\n      return value;\n    }\n  }\n\n  private Object getBooleanProperty() {\n    String value = mPropertyValue.getText().toString();\n    if (value.equals(\"true\") || value.equals(\"True\")) {\n      return true;\n    } else if (value.equals(\"false\") || value.equals(\"False\")) {\n      return false;\n    }\n    Toast.makeText(mContext, \"boolean should either be true or false\", Toast.LENGTH_LONG).show();\n    return null;\n  }\n\n  @Override\n  public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n    selectedPropertyType = position;\n    List<String> properties = Arrays.asList(propertyTypes);\n    if (selectedPropertyType == properties.indexOf(\"date\")) {\n      Calendar calendar = Calendar.getInstance();\n      final DatePickerDialog datePickerDialog = new DatePickerDialog(mContext, (view14, year, monthOfYear, dayOfMonth) -> {\n        mPropertyValue.setText(String.format(Locale.getDefault(), \"%d/%d/%d\", monthOfYear + 1, dayOfMonth, year));\n        Calendar pickedCalendar = Calendar.getInstance();\n        pickedCalendar.set(year, monthOfYear, dayOfMonth);\n        lastDatePicked = pickedCalendar.getTime();\n      }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));\n\n      mPropertyValue.setOnClickListener(view13 -> {\n        if (!datePickerDialog.isShowing()) {\n          datePickerDialog.show();\n        }\n      });\n\n      mPropertyValue.setOnTouchListener((view12, event) -> {\n        mPropertyValue.performClick();\n        return true;\n      });\n\n      mPropertyValue.setOnKeyListener((view1, keyCode, event) -> {\n        mPropertyValue.performClick();\n        return true;\n      });\n    } else {\n      mPropertyValue.setOnClickListener(null);\n      mPropertyValue.setOnTouchListener(null);\n      mPropertyValue.setOnKeyListener(null);\n    }\n  }\n\n  @Override\n  public void onNothingSelected(AdapterView<?> parent) {\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/subscriptions/EmailSubscriptionStateDialog.java",
    "content": "package com.appboy.sample.subscriptions;\n\nimport android.util.Log;\nimport android.view.View;\nimport android.widget.Toast;\n\nimport com.braze.enums.NotificationSubscriptionType;\nimport com.braze.Braze;\nimport com.braze.support.BrazeLogger;\n\npublic class EmailSubscriptionStateDialog extends SubscriptionStateDialogBase {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(EmailSubscriptionStateDialog.class);\n\n  @Override\n  public void onExitButtonPressed(boolean positiveResult) {\n    if (positiveResult) {\n      int subscriptionStateId = mSubscriptionState.getCheckedRadioButtonId();\n      View subscriptionStateRadioButton = mSubscriptionState.findViewById(subscriptionStateId);\n      int subscriptionState = mSubscriptionState.indexOfChild(subscriptionStateRadioButton);\n      switch (subscriptionState) {\n        case SUBSCRIBED_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setEmailNotificationSubscriptionType(NotificationSubscriptionType.SUBSCRIBED);\n          Toast.makeText(getContext(), \"Set email subscription state to subscribed.\", Toast.LENGTH_SHORT).show();\n          break;\n        case OPTED_IN_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setEmailNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN);\n          Toast.makeText(getContext(), \"Set email subscription state to opted-in.\", Toast.LENGTH_SHORT).show();\n          break;\n        case UNSUBSCRIBED_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setEmailNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED);\n          Toast.makeText(getContext(), \"Set email subscription state to unsubscribed.\", Toast.LENGTH_SHORT).show();\n          break;\n        default:\n          Log.w(TAG, \"Error parsing subscription state: \" + subscriptionState);\n          Toast.makeText(getContext(), \"Error parsing subscription state: \" + subscriptionState, Toast.LENGTH_SHORT).show();\n      }\n    }\n    this.dismiss();\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/subscriptions/PushSubscriptionStateDialog.java",
    "content": "package com.appboy.sample.subscriptions;\n\nimport android.util.Log;\nimport android.view.View;\nimport android.widget.Toast;\n\nimport com.braze.enums.NotificationSubscriptionType;\nimport com.braze.Braze;\nimport com.braze.support.BrazeLogger;\n\npublic class PushSubscriptionStateDialog extends SubscriptionStateDialogBase {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(PushSubscriptionStateDialog.class);\n\n  @Override\n  public void onExitButtonPressed(boolean positiveResult) {\n    if (positiveResult) {\n      int subscriptionStateId = mSubscriptionState.getCheckedRadioButtonId();\n      View subscriptionStateRadioButton = mSubscriptionState.findViewById(subscriptionStateId);\n      int subscriptionState = mSubscriptionState.indexOfChild(subscriptionStateRadioButton);\n      switch (subscriptionState) {\n        case SUBSCRIBED_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setPushNotificationSubscriptionType(NotificationSubscriptionType.SUBSCRIBED);\n          Toast.makeText(getContext(), \"Set push subscription state to subscribed.\", Toast.LENGTH_SHORT).show();\n          break;\n        case OPTED_IN_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setPushNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN);\n          Toast.makeText(getContext(), \"Set push subscription state to opted-in.\", Toast.LENGTH_SHORT).show();\n          break;\n        case UNSUBSCRIBED_INDEX:\n          Braze.getInstance(getContext()).getCurrentUser().setPushNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED);\n          Toast.makeText(getContext(), \"Set push subscription state to unsubscribed.\", Toast.LENGTH_SHORT).show();\n          break;\n        default:\n          Log.w(TAG, \"Error parsing subscription state: \" + subscriptionState);\n          Toast.makeText(getContext(), \"Error parsing subscription state: \" + subscriptionState, Toast.LENGTH_SHORT).show();\n      }\n    }\n    this.dismiss();\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/subscriptions/SubscriptionStateDialogBase.java",
    "content": "package com.appboy.sample.subscriptions;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.RadioGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.appboy.sample.R;\nimport com.appboy.sample.dialog.CustomDialogBase;\n\npublic abstract class SubscriptionStateDialogBase extends CustomDialogBase {\n  protected static final int SUBSCRIBED_INDEX = 0;\n  protected static final int OPTED_IN_INDEX = 1;\n  protected static final int UNSUBSCRIBED_INDEX = 2;\n\n  protected RadioGroup mSubscriptionState;\n\n  @Nullable\n  @Override\n  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n    return inflater.inflate(R.layout.subscription_state_preferences, container, false);\n  }\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n    mSubscriptionState = view.findViewById(R.id.subscription_state);\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/BrazeActionTestingUtil.kt",
    "content": "package com.appboy.sample.util\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.net.Uri\nimport com.braze.enums.inappmessage.ClickAction\nimport com.braze.enums.inappmessage.DismissType\nimport com.braze.models.inappmessage.IInAppMessage\nimport com.braze.models.inappmessage.InAppMessageModal\nimport com.braze.models.inappmessage.MessageButton\n\nobject BrazeActionTestingUtil {\n\n    @JvmStatic\n    fun getPushPromptInAppMessageModal(context: Context): IInAppMessage {\n        val pushPromptModal = InAppMessageModal().apply {\n            backgroundColor = Color.LTGRAY\n            dismissType = DismissType.MANUAL\n            header = \"Allow Braze Push\"\n            message = \"No-Code Braze Actions push primer. Try it out!\"\n        }\n        pushPromptModal.messageButtons = listOf(\n            MessageButton().apply {\n                text = \"Cancel :(\"\n                setClickBehavior(ClickAction.NONE)\n                backgroundColor = Color.BLACK\n            },\n            MessageButton().apply {\n                text = \"Show Me Push!\"\n                setClickBehavior(ClickAction.URI, getPushPromptUri(context))\n            }\n        )\n        return pushPromptModal\n    }\n\n    private fun getPushPromptUri(context: Context): Uri {\n        val filename = \"braze_actions/show_push_prompt.txt\"\n        val contents = context.assets\n            .open(filename)\n            .bufferedReader()\n            .use {\n                it.readText()\n            }\n        return Uri.parse(contents)\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/ButtonUtils.java",
    "content": "package com.appboy.sample.util;\n\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.EditText;\n\npublic class ButtonUtils {\n\n  public static void setUpPopulateButton(View view, int buttonId, final EditText output, final String storedString) {\n    setUpPopulateButton(view, buttonId, output, storedString, null, null);\n  }\n\n  public static Button setUpPopulateButton(View view, int buttonId, final EditText keyOutput, final String keyStoredString, final EditText valueOutput, final String valueStoredString) {\n    final Button populateButton = view.findViewById(buttonId);\n    populateButton.setOnClickListener(view1 -> {\n      handlePopulateButtonClick(keyOutput, keyStoredString);\n      if (valueOutput != null && valueStoredString != null) {\n        handlePopulateButtonClick(valueOutput, valueStoredString);\n      }\n    });\n    return populateButton;\n  }\n\n  public static void handlePopulateButtonClick(EditText buttonEditText, String defaultText) {\n    if (buttonEditText.getText().length() == 0) {\n      buttonEditText.setText(defaultText);\n    } else {\n      buttonEditText.getText().clear();\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/ContentCardsTestingUtil.kt",
    "content": "package com.appboy.sample.util\n\nimport android.content.Context\nimport com.braze.Braze\nimport com.braze.enums.CardKey\nimport com.braze.enums.CardType\nimport com.braze.models.cards.Card\nimport org.json.JSONObject\nimport java.util.*\nimport java.util.concurrent.TimeUnit\n\n@Suppress(\"UnsafeCallOnNullableType\")\nclass ContentCardsTestingUtil private constructor() {\n    companion object {\n        private const val CARD_URL = \"https://braze.com\"\n\n        /**\n         * https://effigis.com/en/solutions/satellite-images/satellite-image-samples/\n         */\n        private val SUPER_HIGH_RESOLUTION_IMAGES = listOf(\n            \"https://effigis.com/wp-content/uploads/2015/02/Airbus_Pleiades_50cm_8bit_RGB_Yogyakarta.jpg\",\n            \"https://effigis.com/wp-content/uploads/2015/02/DigitalGlobe_WorldView2_50cm_8bit_Pansharpened_RGB_DRA_Rome_Italy_2009DEC10_8bits_sub_r_1.jpg\",\n            \"https://effigis.com/wp-content/uploads/2015/02/GeoEye_Ikonos_1m_8bit_RGB_DRA_Oil_2005NOV25_8bits_r_1.jpg\"\n        )\n        private val random = Random()\n\n        fun createRandomCards(context: Context, numCardsOfEachType: Int): List<Card> {\n            val cards = mutableListOf<Card>()\n\n            for (cardType in CardType.values()) {\n                if (cardType == CardType.DEFAULT) {\n                    continue\n                }\n                repeat((0..numCardsOfEachType).count()) {\n                    createRandomCard(context, cardType)?.let { card -> cards.add(card) }\n                }\n            }\n\n            cards.shuffle()\n            return cards\n        }\n\n        fun getRemovedCardJson(id: String): JSONObject {\n            val ccp = CardKey.Provider(true)\n            return JSONObject(\n                mapOf(\n                    ccp.getKey(CardKey.ID) to id,\n                    ccp.getKey(CardKey.REMOVED) to true,\n                )\n            )\n        }\n\n        fun createCaptionedImageCardJson(\n            id: String,\n            title: String,\n            description: String,\n            imageUrl: String\n        ): JSONObject {\n            val ccp = CardKey.Provider(true)\n\n            // Get the default fields\n            val defaultMapping = getDefaultCardFields(ccp, CardType.CAPTIONED_IMAGE)\n\n            defaultMapping.mergeWith(\n                mapOf(\n                    ccp.getKey(CardKey.ID) to id,\n                    ccp.getKey(CardKey.CAPTIONED_IMAGE_IMAGE) to imageUrl,\n                    ccp.getKey(CardKey.CAPTIONED_IMAGE_ASPECT_RATIO) to 1.0,\n                    ccp.getKey(CardKey.CAPTIONED_IMAGE_TITLE) to title,\n                    ccp.getKey(CardKey.CAPTIONED_IMAGE_DESCRIPTION) to description,\n                    ccp.getKey(CardKey.PINNED) to true,\n                    ccp.getKey(CardKey.DISMISSIBLE) to false,\n                    ccp.getKey(CardKey.CREATED) to System.currentTimeMillis()\n                )\n            )\n            return JSONObject(defaultMapping.toMap())\n        }\n\n        private fun getDefaultCardFields(ccp: CardKey.Provider, cardType: CardType): MutableMap<String, Any> = mutableMapOf(\n            ccp.getKey(CardKey.ID) to getRandomString(),\n            ccp.getKey(CardKey.TYPE) to ccp.getServerKeyFromCardType(cardType)!!,\n            ccp.getKey(CardKey.VIEWED) to getRandomBoolean(),\n            ccp.getKey(CardKey.CREATED) to getNow(),\n            ccp.getKey(CardKey.EXPIRES_AT) to getNowPlusDelta(TimeUnit.DAYS, 30),\n            ccp.getKey(CardKey.OPEN_URI_IN_WEBVIEW) to getRandomBoolean(),\n            ccp.getKey(CardKey.DISMISSED) to false,\n            ccp.getKey(CardKey.REMOVED) to false,\n            ccp.getKey(CardKey.PINNED) to getRandomBoolean(),\n            ccp.getKey(CardKey.DISMISSIBLE) to getRandomBoolean(),\n            ccp.getKey(CardKey.IS_TEST) to true\n        )\n\n        private fun createRandomCard(context: Context, cardType: CardType): Card? {\n            val ccp = CardKey.Provider(true)\n\n            // Set the default fields\n            val defaultMapping = getDefaultCardFields(ccp, cardType)\n\n            // Based on the card type, add new fields\n            val title = \"Title\"\n            val description = \"Description -> cardType $cardType\"\n            val randomImage = getRandomImageUrl()\n\n            when (cardType) {\n                CardType.BANNER -> {\n                    defaultMapping.mergeWith(\n                        mapOf(\n                            ccp.getKey(CardKey.BANNER_IMAGE_IMAGE) to randomImage.first,\n                            ccp.getKey(CardKey.BANNER_IMAGE_ASPECT_RATIO) to randomImage.second,\n                            ccp.getKey(CardKey.BANNER_IMAGE_URL) to CARD_URL\n                        )\n                    )\n                }\n                CardType.CAPTIONED_IMAGE -> {\n                    defaultMapping.mergeWith(\n                        mapOf(\n                            ccp.getKey(CardKey.CAPTIONED_IMAGE_IMAGE) to randomImage.first,\n                            ccp.getKey(CardKey.CAPTIONED_IMAGE_ASPECT_RATIO) to randomImage.second,\n                            ccp.getKey(CardKey.CAPTIONED_IMAGE_TITLE) to title,\n                            ccp.getKey(CardKey.CAPTIONED_IMAGE_DESCRIPTION) to description\n                        )\n                    )\n                    if (random.nextBoolean()) {\n                        defaultMapping.mergeWith(\n                            mapOf(\n                                ccp.getKey(CardKey.CAPTIONED_IMAGE_URL) to CARD_URL\n                            )\n                        )\n                    }\n                }\n                CardType.SHORT_NEWS -> {\n                    defaultMapping.mergeWith(\n                        mapOf(\n                            ccp.getKey(CardKey.SHORT_NEWS_IMAGE) to randomImage.first,\n                            ccp.getKey(CardKey.SHORT_NEWS_TITLE) to title,\n                            ccp.getKey(CardKey.SHORT_NEWS_DESCRIPTION) to description\n                        )\n                    )\n                    if (random.nextBoolean()) {\n                        defaultMapping.mergeWith(\n                            mapOf(\n                                ccp.getKey(CardKey.SHORT_NEWS_URL) to CARD_URL\n                            )\n                        )\n                    }\n                }\n                CardType.TEXT_ANNOUNCEMENT -> {\n                    defaultMapping.mergeWith(\n                        mapOf(\n                            ccp.getKey(CardKey.TEXT_ANNOUNCEMENT_DESCRIPTION) to description,\n                            ccp.getKey(CardKey.TEXT_ANNOUNCEMENT_TITLE) to title\n                        )\n                    )\n                    if (random.nextBoolean()) {\n                        defaultMapping.mergeWith(\n                            mapOf(\n                                ccp.getKey(CardKey.TEXT_ANNOUNCEMENT_URL) to CARD_URL\n                            )\n                        )\n                    }\n                }\n                else -> {\n                    // Do nothing!\n                }\n            }\n\n            val json = JSONObject(defaultMapping.toMap())\n            return Braze.getInstance(context).deserializeContentCard(json)\n        }\n\n        private fun getRandomString(): String = UUID.randomUUID().toString()\n\n        private fun getRandomBoolean(): Boolean = random.nextBoolean()\n\n        // Get now plus some random delta a minute into the future\n        private fun getNow(): Long = getNowPlusDelta(TimeUnit.MILLISECONDS, random.nextInt(60000).toLong())\n\n        private fun getNowPlusDelta(deltaUnits: TimeUnit, delta: Long): Long = System.currentTimeMillis() + deltaUnits.toMillis(delta)\n\n        /**\n         * @return Pair of url to aspect ratio\n         */\n        private fun getRandomImageUrl(): Pair<String, Double> {\n            return if (random.nextInt(100) < 40) {\n                // Return a SUPER high resolution image\n                val url = \"${SUPER_HIGH_RESOLUTION_IMAGES.shuffled(random).first()}?q=${System.nanoTime()}\"\n                Pair(url, 1.0)\n            } else {\n                val height = random.nextInt(500) + 200\n                val width = random.nextInt(500) + 200\n                Pair(\"https://picsum.photos/seed/${System.nanoTime()}/$width/$height\", width.toDouble() / height.toDouble())\n            }\n        }\n\n        /**\n         * Merges the content of a target map with another map\n         */\n        private fun MutableMap<String, Any>.mergeWith(another: Map<String, Any>) {\n            for ((key, value) in another) {\n                this[key] = value\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/EmulatorDetectionUtils.java",
    "content": "package com.appboy.sample.util;\n\npublic class EmulatorDetectionUtils {\n  public static final String AVD_X86_MODEL = \"Android SDK built for x86\";\n  public static final String AVD_X86_64_MODEL = \"Android SDK built for x86_64\";\n\n  public static String[] getEmulatorModelsForAppboyDeactivation() {\n    return new String[]{AVD_X86_MODEL, AVD_X86_64_MODEL};\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/EnvironmentUtils.kt",
    "content": "package com.appboy.sample.util\n\nimport android.R\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.appcompat.app.AlertDialog\nimport com.appboy.sample.DroidboyApplication\nimport com.braze.support.BrazeLogger.Priority.E\nimport com.braze.support.BrazeLogger.Priority.I\nimport com.braze.support.BrazeLogger.brazelog\nimport com.google.mlkit.vision.barcode.Barcode\nimport com.google.mlkit.vision.barcode.BarcodeScannerOptions\nimport com.google.mlkit.vision.barcode.BarcodeScanning\nimport com.google.mlkit.vision.common.InputImage\n\nclass EnvironmentUtils private constructor() {\n    companion object {\n        private const val BRAZE_ENVIRONMENT_DEEPLINK_SCHEME_PATH = \"braze://environment\"\n        private const val BRAZE_ENVIRONMENT_DEEPLINK_ENDPOINT = \"endpoint\"\n        private const val BRAZE_ENVIRONMENT_DEEPLINK_API_KEY = \"api_key\"\n\n        @JvmStatic\n        fun analyzeBitmapForEnvironmentBarcode(activity: Activity, bitmap: Bitmap) {\n            // Build the barcode detector\n            val options = BarcodeScannerOptions.Builder()\n                .setBarcodeFormats(Barcode.FORMAT_QR_CODE)\n                .build()\n            val image = InputImage.fromBitmap(bitmap, 0)\n            val scanner = BarcodeScanning.getClient(options)\n            scanner.process(image)\n                .addOnSuccessListener { barcodes: List<Barcode> ->\n                    if (barcodes.isEmpty()) {\n                        showToast(activity, \"Couldn't find barcode. Please try again!\")\n                    } else {\n                        for (barcode in barcodes) {\n                            val rawValue = barcode.rawValue\n                            if (rawValue?.startsWith(BRAZE_ENVIRONMENT_DEEPLINK_SCHEME_PATH) == true) {\n                                showToast(activity, \"Found barcode: $rawValue\")\n                                setEnvironmentViaDeepLink(activity, rawValue)\n                            }\n                        }\n                    }\n                }\n                .addOnFailureListener { e: Exception -> brazelog(E, e) { \"Failed to parse barcode bitmap\" } }\n                .addOnCompleteListener { bitmap.recycle() }\n        }\n\n        /**\n         * Braze deep link in the form\n         * braze://environment?endpoint=ENDPOINT_HERE&api_key=API_KEY_HERE\n         */\n        @SuppressLint(\"ApplySharedPref\")\n        private fun setEnvironmentViaDeepLink(context: Activity, environmentText: String) {\n            val uri = Uri.parse(environmentText)\n            val endpoint = uri.getQueryParameter(BRAZE_ENVIRONMENT_DEEPLINK_ENDPOINT)\n            val apiKey = uri.getQueryParameter(BRAZE_ENVIRONMENT_DEEPLINK_API_KEY)\n\n            brazelog(I) { \"Using environment endpoint: $endpoint\" }\n            brazelog(I) { \"Using environment api key: $apiKey\" }\n            val message = StringBuilder()\n                .append(\"Looks correct? 👌\")\n                .append(\"\\n\\n\")\n                .append(\"New environment endpoint: \")\n                .append(\"\\n\")\n                .append(endpoint)\n                .append(\"\\n\\n\")\n                .append(\"New environment api key: \")\n                .append(\"\\n\")\n                .append(apiKey)\n            AlertDialog.Builder(context)\n                .setTitle(\"Changing Droidboy environment\")\n                .setMessage(message.toString())\n                .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->\n                    val sharedPreferencesEditor: SharedPreferences.Editor = getPrefs(context).edit()\n                    sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_API_KEY_PREF_KEY, apiKey)\n                    sharedPreferencesEditor.putString(DroidboyApplication.OVERRIDE_ENDPOINT_PREF_KEY, endpoint)\n                    sharedPreferencesEditor.commit()\n                    LifecycleUtils.restartApp(context)\n                } // A null listener allows the button to dismiss the dialog and take no further action.\n                .setNegativeButton(R.string.cancel, null)\n                .setIcon(R.drawable.ic_dialog_info)\n                .show()\n        }\n\n        private fun getPrefs(context: Context): SharedPreferences = context.getSharedPreferences(\n            context.getString(com.appboy.sample.R.string.shared_prefs_location),\n            Context.MODE_PRIVATE\n        )\n\n        private fun showToast(context: Context, message: String) {\n            Toast.makeText(context, message, Toast.LENGTH_LONG).show()\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/LifecycleUtils.java",
    "content": "package com.appboy.sample.util;\n\nimport android.app.AlarmManager;\nimport android.app.PendingIntent;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Handler;\n\nimport com.appboy.sample.activity.DroidBoyActivity;\nimport com.braze.support.BrazeLogger;\nimport com.braze.support.IntentUtils;\n\n@SuppressWarnings(\"PMD.DoNotCallSystemExit\")\npublic class LifecycleUtils {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(LifecycleUtils.class);\n\n  public static void restartApp(Context context) {\n    new Handler(context.getMainLooper()).postDelayed(() -> {\n      Intent startActivity = new Intent(context, DroidBoyActivity.class);\n      int pendingIntentId = 109829837;\n      final int flags = PendingIntent.FLAG_CANCEL_CURRENT | IntentUtils.getImmutablePendingIntentFlags();\n      PendingIntent pendingIntent = PendingIntent.getActivity(context, pendingIntentId, startActivity, flags);\n      AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);\n      alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pendingIntent);\n      BrazeLogger.i(TAG, \"Restarting application to apply new environment values\");\n      System.exit(0);\n    }, 500);\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/LogcatExportUtil.kt",
    "content": "package com.appboy.sample.util\n\nimport android.content.Context\nimport android.net.Uri\nimport android.util.Log\nimport androidx.core.content.FileProvider\nimport java.io.File\nimport java.io.IOException\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nclass LogcatExportUtil private constructor() {\n    companion object {\n        private val TAG = this::class.java.simpleName\n\n        /**\n         * Our logcat command\n         * -d states for the command to completely dump to our buffer, then return\n         * -v threadtime sets the output log format. See https://developer.android.com/studio/command-line/logcat.html#outputFormat\n         */\n        private val LOGCAT_CAPTURE_COMMAND = arrayOf(\"logcat\", \"-d\", \"-v\", \"threadtime\")\n        private val LOGCAT_CLEAR_COMMAND = arrayOf(\"logcat\", \"-c\")\n        private const val LOGCAT_EXPORT_DIRECTORY = \"logcat_files\"\n\n        /**\n         * Exports the logcat to a file and returns a FileProvider uri to that created file\n         */\n        fun exportLogcatToFile(context: Context): Uri {\n            // Write the logcat to a file in internal storage\n            val currentFormattedDate = getCurrentFormattedDate()\n            val logcatExportDirectory = File(context.cacheDir, LOGCAT_EXPORT_DIRECTORY)\n            logcatExportDirectory.mkdirs()\n            val outputFile = File(logcatExportDirectory, \"logcat export - $currentFormattedDate.txt\")\n            outputFile.writeText(getLogcat())\n            return FileProvider.getUriForFile(context, \"${context.packageName}.fileprovider\", outputFile)\n        }\n\n        private fun getLogcat(): String {\n            // The process id is used to filter the messages. This Test runner and the test itself run in the same process.\n            val currentProcessId = android.os.Process.myPid().toString()\n            val process = Runtime.getRuntime().exec(LOGCAT_CAPTURE_COMMAND)\n\n            var firstLine = \"\"\n            val stringBuilder = StringBuilder()\n            process.inputStream.bufferedReader().forEachLine {\n                if (it.contains(currentProcessId)) {\n                    if (firstLine.isEmpty()) {\n                        firstLine = it\n                    }\n                    stringBuilder.append(it)\n                    stringBuilder.append('\\n')\n                }\n            }\n\n            // Once the logcat is cleared, the next run of this logcat reader will have a\n            // much shorter buffer to deal with, resulting in a faster read time.\n            clearLogcat()\n\n            return stringBuilder.toString()\n        }\n\n        private fun clearLogcat() {\n            try {\n                Runtime.getRuntime().exec(LOGCAT_CLEAR_COMMAND)\n            } catch (e: IOException) {\n                Log.e(TAG, \"Failed to close logcat\", e)\n            }\n        }\n\n        private fun getCurrentFormattedDate(): String {\n            val simpleDateFormat = SimpleDateFormat(\"MM-dd kk:mm:ss.SSS\", Locale.US)\n            simpleDateFormat.timeZone = TimeZone.getDefault()\n            return simpleDateFormat.format(Date(System.currentTimeMillis()))\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/RuntimePermissionUtils.kt",
    "content": "package com.appboy.sample.util\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.os.Build\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.appcompat.app.AlertDialog\nimport com.appboy.sample.R\n\nobject RuntimePermissionUtils {\n    /**\n     * If needed, shows a \"here's why we need location permissions\" prompt\n     * before actually requesting a single location permission.\n     */\n    @JvmStatic\n    fun requestLocationPermission(\n        activity: Activity,\n        permission: String,\n        singlePermissionLauncher: ActivityResultLauncher<String?>\n    ) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n            return\n        }\n        if (activity.shouldShowRequestPermissionRationale(permission)) {\n            AlertDialog.Builder(activity)\n                .setTitle(\n                    if (Manifest.permission.ACCESS_BACKGROUND_LOCATION == permission) {\n                        R.string.droidboy_required_bg_location_prompt_title\n                    } else {\n                        R.string.droidboy_required_location_prompt_title\n                    }\n                )\n                .setMessage(\n                    if (Manifest.permission.ACCESS_BACKGROUND_LOCATION == permission) {\n                        R.string.droidboy_required_bg_location_prompt_message\n                    } else {\n                        R.string.droidboy_required_location_prompt_message\n                    }\n                )\n                .setPositiveButton(\"allow\") { _: DialogInterface?, _: Int ->\n                    singlePermissionLauncher.launch(\n                        permission\n                    )\n                }\n                .setNegativeButton(\"no\", null)\n                .setIcon(android.R.drawable.ic_dialog_map)\n                .show()\n        } else {\n            singlePermissionLauncher.launch(permission)\n        }\n    }\n\n    /**\n     * If needed, shows a \"here's why we need location permissions\" prompt\n     * before actually requesting the set of location permissions.\n     */\n    @JvmStatic\n    fun requestLocationPermissions(\n        activity: Activity,\n        permissions: Array<String?>,\n        multiplePermissionLauncher: ActivityResultLauncher<Array<String?>?>\n    ) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || permissions.size <= 1) {\n            return\n        }\n        var isExplanationNeeded = false\n        for (permission in permissions) {\n            if (permission != null && activity.shouldShowRequestPermissionRationale(permission)) {\n                isExplanationNeeded = true\n                break\n            }\n        }\n        if (isExplanationNeeded) {\n            AlertDialog.Builder(activity)\n                .setTitle(R.string.droidboy_required_location_prompt_title)\n                .setMessage(R.string.droidboy_required_location_prompt_message)\n                .setPositiveButton(\"allow\") { _: DialogInterface?, _: Int ->\n                    multiplePermissionLauncher.launch(\n                        permissions\n                    )\n                }\n                .setNegativeButton(\"no\", null)\n                .setIcon(android.R.drawable.ic_dialog_map)\n                .show()\n        } else {\n            multiplePermissionLauncher.launch(permissions)\n        }\n    }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/SpinnerUtils.java",
    "content": "package com.appboy.sample.util;\n\nimport android.widget.AdapterView;\nimport android.widget.AdapterView.OnItemSelectedListener;\nimport android.widget.ArrayAdapter;\nimport android.widget.Spinner;\n\nimport com.appboy.sample.R;\n\nimport java.util.List;\n\npublic class SpinnerUtils {\n  public static final String NOT_SET = \"not set\";\n\n  public static void setUpSpinner(Spinner spinner, OnItemSelectedListener listener, int arrayId) {\n    ArrayAdapter arrayAdapter = ArrayAdapter.createFromResource(spinner.getContext(), arrayId, R.layout.spinner_item);\n    arrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);\n    spinner.setAdapter(arrayAdapter);\n    spinner.setOnItemSelectedListener(listener);\n  }\n\n  public static void setUpSpinnerWithList(Spinner spinner, OnItemSelectedListener listener, List list) {\n    ArrayAdapter arrayAdapter = new ArrayAdapter(spinner.getContext(), R.layout.spinner_item, list);\n    arrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);\n    spinner.setAdapter(arrayAdapter);\n    spinner.setOnItemSelectedListener(listener);\n  }\n\n  public static boolean spinnerItemNotSet(String spinnerItem) {\n    return (spinnerItem == null || spinnerItem.equalsIgnoreCase(NOT_SET));\n  }\n\n  public static String handleSpinnerItemSelected(AdapterView<?> parent, int arrayId) {\n    String spinnerItem = parent.getResources().getStringArray(arrayId)[parent.getSelectedItemPosition()];\n    if (spinnerItem != null && spinnerItem.length() > 0) {\n      return spinnerItem;\n    } else {\n      return SpinnerUtils.NOT_SET;\n    }\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/java/com/appboy/sample/util/ViewUtils.java",
    "content": "package com.appboy.sample.util;\n\nimport android.os.Build;\nimport android.view.View;\nimport android.view.Window;\nimport android.view.WindowManager;\n\n@SuppressWarnings(\"deprecation\")\npublic class ViewUtils {\n\n  /**\n   * Enables Immersive mode. Basically means the app becomes fullscreen and the notification bar fades away.\n   */\n  public static void enableImmersiveMode(final View decorView) {\n    decorView.setSystemUiVisibility(setSystemUiVisibility());\n    decorView.setOnSystemUiVisibilityChangeListener(visibility -> {\n      if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {\n        decorView.setSystemUiVisibility(setSystemUiVisibility());\n      }\n    });\n  }\n\n  public static void enableNoLimitsMode(Window window) {\n    window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);\n  }\n\n  private static int setSystemUiVisibility() {\n    int visibilityFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION\n        | View.SYSTEM_UI_FLAG_FULLSCREEN;\n\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n      visibilityFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;\n    }\n    return visibilityFlags;\n  }\n}\n"
  },
  {
    "path": "droidboy/src/main/res/drawable/custom_inappmessage_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <item\n    android:id=\"@+id/inappmessage_background\"\n    android:left=\"10.0dp\"\n    android:top=\"5.0dp\"\n    android:right=\"10.0dp\"\n    android:bottom=\"5.0dp\">\n    <shape>\n        <solid android:color=\"@color/custom_inappmessage_green\"/>\n        <corners android:radius=\"3.0dp\"/>\n    </shape>\n  </item>\n</layer-list>"
  },
  {
    "path": "droidboy/src/main/res/font/bungee_font_family.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:android=\"http://schemas.android.com/apk/res/android\"\n             xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n  <!-- Note: You must declare both sets of attributes\n      to ensure your fonts load on devices running Android 8.0 (API level 26) or lower.\n      See https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml.html -->\n  <font android:fontStyle=\"normal\"\n        android:fontWeight=\"400\"\n        android:font=\"@font/bungeeshade\"\n\n        app:fontStyle=\"normal\"\n        app:fontWeight=\"400\"\n        app:font=\"@font/bungeeshade\"/>\n</font-family>\n"
  },
  {
    "path": "droidboy/src/main/res/font/sailec_font_family.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:android=\"http://schemas.android.com/apk/res/android\"\n             xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n  <!-- Note: You must declare both sets of attributes\n      to ensure your fonts load on devices running Android 8.0 (API level 26) or lower.\n      See https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml.html -->\n  <font android:fontStyle=\"normal\"\n        android:fontWeight=\"400\"\n        android:font=\"@font/sailec_regular\"\n\n        app:fontStyle=\"normal\"\n        app:fontWeight=\"400\"\n        app:font=\"@font/sailec_regular\"/>\n\n    <font android:fontStyle=\"normal\"\n      android:fontWeight=\"400\"\n      android:font=\"@font/sailec_bold\"\n\n      app:fontStyle=\"normal\"\n      app:fontWeight=\"400\"\n      app:font=\"@font/sailec_bold\"/>\n\n</font-family>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/activity_in_app_message_sandbox.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:ignore=\"HardcodedText\" >\n  <Button\n    android:id=\"@+id/bSandboxDisplayMessage0\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginBottom=\"32dp\"\n    android:text=\"0 buttons\"\n    app:layout_constraintBottom_toBottomOf=\"parent\"\n    app:layout_constraintEnd_toStartOf=\"@+id/bSandboxDisplayMessage1\"\n    app:layout_constraintHorizontal_bias=\"0.5\"\n    app:layout_constraintStart_toStartOf=\"parent\" />\n  <Button\n    android:id=\"@+id/bSandboxDisplayMessage1\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:text=\"1 Button\"\n    app:layout_constraintBottom_toBottomOf=\"@+id/bSandboxDisplayMessage0\"\n    app:layout_constraintEnd_toStartOf=\"@+id/bSandboxDisplayMessage2\"\n    app:layout_constraintHorizontal_bias=\"0.5\"\n    app:layout_constraintStart_toEndOf=\"@+id/bSandboxDisplayMessage0\" />\n  <Button\n    android:id=\"@+id/bSandboxDisplayMessage2\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:text=\"2 Buttons\"\n    app:layout_constraintBottom_toBottomOf=\"@+id/bSandboxDisplayMessage1\"\n    app:layout_constraintEnd_toEndOf=\"parent\"\n    app:layout_constraintHorizontal_bias=\"0.5\"\n    app:layout_constraintStart_toEndOf=\"@+id/bSandboxDisplayMessage1\"/>\n  <Button\n    android:id=\"@+id/bSandboxDummyButton\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginStart=\"8dp\"\n    android:layout_marginLeft=\"8dp\"\n    android:layout_marginBottom=\"128dp\"\n    android:text=\"Dummy Button!\"\n    app:layout_constraintBottom_toTopOf=\"@+id/bSandboxDisplayMessage0\"\n    app:layout_constraintStart_toStartOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_attribute.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:orientation=\"vertical\">\n  <TableLayout\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"4.0dp\"\n    android:paddingRight=\"4.0dp\"\n    android:stretchColumns=\"*\">\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_attribute_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_user_attribute\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_key\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_value\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_number_attribute_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_user_number_attribute\" />\n      <EditText\n        android:id=\"@+id/custom_number_attribute_key\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n      <EditText\n        android:id=\"@+id/custom_number_attribute_value\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\"\n        android:inputType=\"number\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_attribute_increment_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_user_attribute_increment\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_increment_key\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_increment_value\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\"\n        android:inputType=\"number\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_attribute_unset_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_user_attribute_unset\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_unset_key\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"4\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_attribute_array_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/attribute_array_key\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_array_key\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n      <EditText\n        android:id=\"@+id/custom_attribute_array_value\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"2\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <RadioGroup\n        android:id=\"@+id/custom_attribute_array_radio\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"3\">\n        <RadioButton\n          android:id=\"@+id/custom_attribute_array_set\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/attribute_array_set\" />\n        <RadioButton\n          android:id=\"@+id/custom_attribute_array_add\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/attribute_array_add\" />\n        <RadioButton\n          android:id=\"@+id/custom_attribute_array_remove\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/attribute_array_remove\" />\n      </RadioGroup>\n      <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"1\"\n        android:text=\"@string/attribute_array\" />\n    </TableRow>\n  </TableLayout>\n  <include layout=\"@layout/dialog_footer_navigation\" />\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_event.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:orientation=\"vertical\">\n  <TableLayout\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"4.0dp\"\n    android:paddingRight=\"4.0dp\"\n    android:stretchColumns=\"*\">\n    <include layout=\"@layout/custom_name\" />\n    <include layout=\"@layout/custom_properties\" />\n  </TableLayout>\n  <include layout=\"@layout/dialog_footer_navigation\" />\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_inappmessage.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<com.appboy.sample.CustomInAppMessageView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/inappmessage\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/custom_inappmessage_background\"\n    android:paddingLeft=\"15.0dp\"\n    android:paddingTop=\"15.0dp\"\n    android:paddingRight=\"15.0dp\"\n    android:paddingBottom=\"15.0dp\">\n    <TextView\n        android:id=\"@+id/inappmessage_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textSize=\"120.0sp\"\n        android:textColor=\"@android:color/white\"\n        android:background=\"@android:color/transparent\"\n        android:padding=\"0.0dp\"\n        android:layout_centerHorizontal=\"true\"\n        android:gravity=\"center\"/>\n    <TextView\n        android:id=\"@+id/inappmessage_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50.0dp\"\n        android:padding=\"0.0dp\"\n        android:layout_centerHorizontal=\"true\"\n        android:gravity=\"center\"\n        android:textColor=\"@android:color/white\"\n        android:layout_below=\"@id/inappmessage_icon\"/>\n    <ImageView\n        android:id=\"@+id/inappmessage_image\"\n        android:layout_width=\"270.0dp\"\n        android:layout_height=\"180.0dp\"\n        android:layout_centerHorizontal=\"true\"\n        android:gravity=\"center\"\n        android:layout_below=\"@id/inappmessage_message\"/>\n</com.appboy.sample.CustomInAppMessageView>"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_name.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <TableRow\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <Button\n            android:id=\"@+id/custom_name_button\"\n            style=\"@style/BaseWidget.PopulateButton\"\n            android:text=\"@string/custom_logging_name\"/>\n\n        <EditText\n            android:id=\"@+id/custom_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_span=\"4\"\n            style=\"@style/BaseWidget.UserEmailAddress\"/>\n    </TableRow>\n</merge>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_properties.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <TableRow\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <Spinner\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:id=\"@+id/property_type_spinner\"\n            android:layout_span=\"1\"/>\n        <EditText\n            android:id=\"@+id/property_key\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            style=\"@style/BaseWidget.UserEmailAddress\"\n            android:layout_span=\"2\"/>\n\n        <EditText\n            android:id=\"@+id/property_value\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            style=\"@style/BaseWidget.UserEmailAddress\"\n            android:layout_span=\"2\"/>\n    </TableRow>\n    <TableRow\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <Button\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Add Property\"\n            android:layout_span=\"5\"\n            android:id=\"@+id/add_property_button\"/>\n\n    </TableRow>\n    <TableRow\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:id=\"@+id/property_linear_layout\"\n            android:orientation=\"vertical\"\n            android:layout_span=\"5\"/>\n    </TableRow>\n</merge>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/custom_purchase.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:orientation=\"vertical\">\n  <TableLayout\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"4.0dp\"\n    android:paddingRight=\"4.0dp\"\n    android:stretchColumns=\"*\">\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/purchase_qty_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_custom_purchase_qty\" />\n      <EditText\n        android:id=\"@+id/purchase_qty\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"4\"\n        android:inputType=\"number\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_purchase_currency_code_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_custom_purchase_currency_code\" />\n      <EditText\n        android:id=\"@+id/custom_purchase_currency_code\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"4\" />\n    </TableRow>\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <Button\n        android:id=\"@+id/custom_purchase_price_button\"\n        style=\"@style/BaseWidget.PopulateButton\"\n        android:text=\"@string/custom_logging_custom_purchase_price_code\" />\n      <EditText\n        android:id=\"@+id/custom_purchase_price_code\"\n        style=\"@style/BaseWidget.UserEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_span=\"4\"\n        android:inputType=\"numberDecimal|numberSigned\" />\n    </TableRow>\n    <include layout=\"@layout/custom_name\" />\n    <include layout=\"@layout/custom_properties\" />\n  </TableLayout>\n  <include layout=\"@layout/dialog_footer_navigation\" />\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/data_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\">\n  <Spinner\n      android:id=\"@+id/file_chooser_spinner\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"/>\n  <ScrollView\n      android:id=\"@+id/scroller_id\"\n      android:layout_width=\"fill_parent\"\n      android:layout_height=\"wrap_content\"\n      android:scrollbars=\"vertical\"\n      android:fillViewport=\"true\">\n    <TextView android:layout_width=\"fill_parent\"\n              android:layout_height=\"fill_parent\"\n              android:id=\"@+id/data_dialog_text_view\">\n    </TextView>\n  </ScrollView>\n</LinearLayout>"
  },
  {
    "path": "droidboy/src/main/res/layout/dialog_footer_navigation.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:orientation=\"horizontal\"\n  android:layout_marginTop=\"20dp\">\n  <Button\n    android:id=\"@+id/bDialogNegative\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_weight=\"1\"\n    android:text=\"Close\" />\n  <Button\n    android:id=\"@+id/bDialogPositive\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_weight=\"1\"\n    android:text=\"Flush\" />\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/drawer_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"192dp\"\n    android:background=\"?attr/colorPrimaryDark\"\n    android:padding=\"16dp\"\n    android:theme=\"@style/ThemeOverlay.AppCompat.Dark\"\n    android:orientation=\"vertical\"\n    android:gravity=\"bottom\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Welcome to Droidboy\"\n        android:textAppearance=\"@style/TextAppearance.AppCompat.Body1\"/>\n\n</LinearLayout>"
  },
  {
    "path": "droidboy/src/main/res/layout/feature_flag_fragment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/feature_flag_swipe_container\"\n  android:layout_width=\"fill_parent\"\n  android:layout_height=\"match_parent\">\n  <androidx.recyclerview.widget.RecyclerView\n    android:id=\"@+id/flag_overview_recycler_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\" />\n</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/feature_flag_overview_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:paddingTop=\"15dp\"\n  android:paddingBottom=\"15dp\">\n  <TextView\n    android:id=\"@+id/tvFlagId\"\n    android:layout_width=\"0dp\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginStart=\"8dp\"\n    android:layout_marginLeft=\"8dp\"\n    android:layout_marginTop=\"8dp\"\n    android:layout_marginEnd=\"24dp\"\n    android:layout_marginRight=\"24dp\"\n    app:layout_constraintEnd_toStartOf=\"@+id/tvFlagEnabled\"\n    app:layout_constraintHorizontal_bias=\"0.0\"\n    app:layout_constraintStart_toStartOf=\"parent\"\n    app:layout_constraintTop_toTopOf=\"parent\"\n    tools:text=\"This is the Feature Flag Identifier. It can be really long sometimes but that's not a big deal.\" />\n  <TextView\n    android:id=\"@+id/tvFlagEnabled\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"8dp\"\n    android:layout_marginEnd=\"16dp\"\n    android:layout_marginRight=\"16dp\"\n    tools:text=\"ON\"\n    app:layout_constraintEnd_toStartOf=\"@+id/tvFlagNumProperties\"\n    app:layout_constraintTop_toTopOf=\"parent\" />\n  <TextView\n    android:id=\"@+id/tvFlagNumProperties\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"8dp\"\n    android:layout_marginEnd=\"8dp\"\n    android:layout_marginRight=\"8dp\"\n    app:layout_constraintEnd_toEndOf=\"parent\"\n    app:layout_constraintTop_toTopOf=\"parent\"\n    tools:text=\"9000\" />\n  <TextView\n    android:id=\"@+id/tvFlagProperties\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginStart=\"8dp\"\n    android:layout_marginLeft=\"8dp\"\n    android:layout_marginTop=\"16dp\"\n    android:layout_marginBottom=\"8dp\"\n    android:visibility=\"gone\"\n    app:layout_constraintBottom_toBottomOf=\"parent\"\n    app:layout_constraintStart_toStartOf=\"parent\"\n    app:layout_constraintTop_toBottomOf=\"@+id/tvFlagId\"\n    tools:text=\"flag details json\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/geofences_map.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\">\n  <fragment xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            android:id=\"@+id/map\"\n            android:name=\"com.google.android.gms.maps.SupportMapFragment\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"/>\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/inappmessage_tester.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:ignore=\"RtlHardcoded\">\n  <LinearLayout\n    android:id=\"@+id/inappmessage_buttons\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_alignParentBottom=\"true\">\n    <Button\n      android:id=\"@+id/create_and_add_inappmessage_button\"\n      style=\"@style/BaseWidget.InAppMessageTesterButton\"\n      android:text=\"@string/create_and_add_inappmessage\" />\n    <Button\n      android:id=\"@+id/display_next_inappmessage_button\"\n      style=\"@style/BaseWidget.InAppMessageTesterButton\"\n      android:text=\"@string/display_next_inappmessage\" />\n    <Button\n      android:id=\"@+id/hide_current_inappmessage_button\"\n      style=\"@style/BaseWidget.InAppMessageTesterButton\"\n      android:text=\"@string/hide_current_inappmessage\" />\n  </LinearLayout>\n  <ScrollView\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_above=\"@id/inappmessage_buttons\"\n    android:layout_alignParentTop=\"true\"\n    android:fillViewport=\"true\">\n    <LinearLayout\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:layout_marginLeft=\"10dp\"\n      android:orientation=\"vertical\">\n      <Spinner\n        android:id=\"@+id/inapp_set_message_type_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_click_action_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_dismiss_type_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_slide_from_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_uri_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_icon_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_image_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_button_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_message_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_header_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_frame_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <Spinner\n        android:id=\"@+id/inapp_orientation_spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n      <TableLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"15dp\"\n        android:stretchColumns=\"1\">\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            android:text=\"@string/set_open_uri_in_webview\"\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"/>\n          <Spinner\n            android:id=\"@+id/inapp_open_uri_in_webview_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"/>\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_background_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_background_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_icon_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_icon_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_icon_background_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_icon_background_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_close_button_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_close_button_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_text_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_text_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_header_text_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_header_text_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_button_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_button_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_button_border_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_button_border_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_header_align\" />\n          <Spinner\n            android:id=\"@+id/inapp_header_align_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_message_align\" />\n          <Spinner\n            android:id=\"@+id/inapp_message_align_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_button_text_color\" />\n          <Spinner\n            android:id=\"@+id/inapp_button_text_color_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_animate_in\" />\n          <Spinner\n            android:id=\"@+id/inapp_animate_in_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n        <TableRow\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\">\n          <TextView\n            style=\"@style/BaseWidget.InAppMessageTesterTextView\"\n            android:text=\"@string/set_animate_out\" />\n          <Spinner\n            android:id=\"@+id/inapp_animate_out_spinner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n        </TableRow>\n      </TableLayout>\n      <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"15dp\"\n        android:orientation=\"vertical\">\n        <CheckBox\n          android:id=\"@+id/custom_inappmessage_view_factory_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_iinappmessage_view_factory\" />\n        <CheckBox\n          android:id=\"@+id/custom_inappmessage_manager_listener_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_iinappmessagemanager_listener\" />\n        <CheckBox\n          android:id=\"@+id/custom_appboy_navigator_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_iappboy_navigator\" />\n        <CheckBox\n          android:id=\"@+id/custom_appboy_animation_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_iappboy_animation\" />\n        <CheckBox\n          android:id=\"@+id/custom_appboy_html_inappmessage_action_listener_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_html_inappmessage_action_listener\" />\n        <CheckBox\n          android:id=\"@+id/custom_appboy_graphic_modal_max_size_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_inappmessage_modal_size\" />\n        <CheckBox\n          android:id=\"@+id/custom_appboy_image_radius_checkbox\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/custom_inappmessage_image_radius\" />\n        <CheckBox\n          android:id=\"@+id/disable_back_button_dismiss_behavior\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/disable_back_button_dismiss_behavior\" />\n        <CheckBox\n          android:id=\"@+id/enable_tap_outside_modal_dismiss_behavior\"\n          style=\"@style/BaseWidget.InAppMessageTesterCheckBox\"\n          android:text=\"@string/enable_tap_outside_modal_dismiss_behavior\" />\n      </LinearLayout>\n    </LinearLayout>\n  </ScrollView>\n</RelativeLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/landing_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.drawerlayout.widget.DrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/root\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:fitsSystemWindows=\"false\">\n  <androidx.coordinatorlayout.widget.CoordinatorLayout\n    android:id=\"@+id/main_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <com.google.android.material.appbar.AppBarLayout\n      android:id=\"@+id/appbar\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\">\n      <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/actionBarSize\"\n        android:background=\"?attr/colorPrimary\"\n        app:popupTheme=\"@style/ThemeOverlay.AppCompat.Light\">\n        <LinearLayout\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:orientation=\"vertical\"\n          android:gravity=\"center_vertical\">\n          <TextView\n            android:id=\"@+id/toolbar_info_endpoint\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"9sp\"\n            tools:text=\"https://test.url.com/api/v3/data\"/>\n          <TextView\n            android:id=\"@+id/toolbar_info_api_key\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"9sp\"\n            tools:text=\"keys-go-here-1483-4e9f-ac0c-7b40030c8f40\"/>\n        </LinearLayout>\n      </androidx.appcompat.widget.Toolbar>\n      <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tabs\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n    </com.google.android.material.appbar.AppBarLayout>\n    <androidx.viewpager2.widget.ViewPager2\n      android:id=\"@+id/viewpager\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n  </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</androidx.drawerlayout.widget.DrawerLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/log_level_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\">\n    <Spinner\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:id=\"@+id/log_level_spinner\"/>\n</LinearLayout>"
  },
  {
    "path": "droidboy/src/main/res/layout/main_fragment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:fillViewport=\"true\"\n  tools:ignore=\"HardcodedText\">\n\n    <LinearLayout\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:orientation=\"vertical\"\n      android:paddingStart=\"20dp\"\n      android:paddingTop=\"20dp\"\n      android:paddingEnd=\"20dp\"\n      android:paddingBottom=\"250dp\">\n\n        <TextView\n          android:id=\"@+id/user_id_textview\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:ems=\"10\"\n          android:fontFamily=\"@font/sailec_bold\"\n          android:text=\"User ID\" />\n\n        <LinearLayout\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:orientation=\"horizontal\"\n          android:layout_marginBottom=\"4dp\">\n\n            <com.google.android.material.textfield.TextInputLayout\n              android:id=\"@+id/outlinedTextField\"\n              style=\"@style/DroidboyOutlinedBox\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"wrap_content\"\n              android:layout_weight=\"1\"\n              android:hint=\"Enter UserId\"\n              android:layout_marginEnd=\"10dp\"\n              android:layout_marginRight=\"10dp\"\n              android:paddingBottom=\"5dp\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                  android:id=\"@+id/com_appboy_sample_set_user_id_edit_text\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"match_parent\"\n                  style=\"@style/BaseWidget.DroidboyTextInput\"\n                  android:paddingRight=\"8dp\"\n                  android:paddingEnd=\"8dp\"\n                  android:paddingLeft=\"8dp\"\n                  android:paddingStart=\"8dp\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Button\n              android:id=\"@+id/com_appboy_sample_set_user_id_button\"\n              style=\"@style/DroidboyOutlinedButton\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:layout_weight=\"2\"\n              android:insetTop=\"6dp\"\n              android:text=\"Update ID\" />\n        </LinearLayout>\n\n        <TextView\n          android:id=\"@+id/custom_event_text_view\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:ems=\"10\"\n          android:paddingTop=\"10dp\"\n          android:text=\"Custom Event\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <LinearLayout\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:orientation=\"horizontal\"\n          android:layout_marginTop=\"4dp\"\n          android:clipChildren=\"true\"\n          android:layout_marginBottom=\"4dp\">\n\n            <com.google.android.material.textfield.TextInputLayout\n              android:id=\"@+id/com_appboy_sample_custom_event_outlined_text_box\"\n              style=\"@style/DroidboyOutlinedBox\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"wrap_content\"\n              android:layout_marginEnd=\"10dp\"\n              android:layout_marginRight=\"10dp\"\n              android:layout_weight=\"1\"\n              android:hint=\"Enter Custom Event\">\n\n                <AutoCompleteTextView\n                  android:id=\"@+id/com_appboy_sample_custom_event_autocomplete_text_view\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"match_parent\"\n                  style=\"@style/BaseWidget.DroidboyTextInput\"\n                  android:paddingTop=\"12dp\"\n                  android:paddingBottom=\"12dp\"\n                  android:paddingRight=\"8dp\"\n                  android:paddingEnd=\"8dp\"\n                  android:paddingLeft=\"8dp\"\n                  android:paddingStart=\"8dp\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Button\n              android:id=\"@+id/com_appboy_sample_log_custom_event_button\"\n              style=\"@style/DroidboyOutlinedButton\"\n              android:layout_height=\"match_parent\"\n              android:layout_weight=\"2\"\n              android:insetTop=\"6dp\"\n              android:insetBottom=\"0dp\"\n              android:text=\"Log Event\" />\n        </LinearLayout>\n\n        <TextView\n          android:id=\"@+id/purchase_text_view\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:paddingTop=\"10dp\"\n          android:text=\"Purchase\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <LinearLayout\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:orientation=\"horizontal\"\n          android:layout_marginTop=\"4dp\"\n          android:clipChildren=\"false\"\n          android:layout_marginBottom=\"4dp\">\n\n            <com.google.android.material.textfield.TextInputLayout\n              android:id=\"@+id/com_appboy_sample_custom_purchase_outlined_text_box\"\n              style=\"@style/DroidboyOutlinedBox\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:layout_marginEnd=\"10dp\"\n              android:layout_marginRight=\"10dp\"\n              android:layout_weight=\"1\"\n              android:hint=\"Enter Purchase\">\n\n                <AutoCompleteTextView\n                  android:id=\"@+id/com_appboy_sample_purchase_autocomplete_text_view\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"match_parent\"\n                  android:paddingTop=\"12dp\"\n                  android:paddingBottom=\"12dp\"\n                  style=\"@style/BaseWidget.DroidboyTextInput\"\n                  android:paddingRight=\"8dp\"\n                  android:paddingEnd=\"8dp\"\n                  android:paddingLeft=\"8dp\"\n                  android:paddingStart=\"8dp\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Button\n              android:id=\"@+id/com_appboy_sample_log_purchase_button\"\n              style=\"@style/DroidboyOutlinedButton\"\n              android:layout_height=\"match_parent\"\n              android:layout_weight=\"2\"\n              android:insetTop=\"6dp\"\n              android:insetBottom=\"0dp\"\n              android:text=\"Log Purchase\" />\n        </LinearLayout>\n\n        <View\n          android:id=\"@+id/divider\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"1dp\"\n          android:layout_marginTop=\"15dp\"\n          android:background=\"?android:attr/listDivider\" />\n\n        <TextView\n          android:id=\"@+id/user_attribute_text_view\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:paddingTop=\"10dp\"\n          android:text=\"User Attributes\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <LinearLayout\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:orientation=\"horizontal\">\n\n            <Button\n              android:id=\"@+id/com_appboy_sample_set_user_attributes_button\"\n              style=\"@style/DroidboyOutlinedButton\"\n              android:layout_marginEnd=\"10dp\"\n              android:layout_marginRight=\"10dp\"\n              android:layout_weight=\"1\"\n              android:text=\"Set User Attributes\" />\n\n            <Button\n              android:id=\"@+id/com_appboy_sample_unset_user_attributes_button\"\n              style=\"@style/DroidboyOutlinedButton\"\n              android:layout_weight=\"1\"\n              android:text=\"Unset User Attributes\" />\n        </LinearLayout>\n\n        <Button\n          android:id=\"@+id/com_appboy_sample_request_flush_button\"\n          style=\"@style/DroidboyOutlinedButton\"\n          android:layout_width=\"200dp\"\n          android:text=\"Request Flush\" />\n\n        <View\n          android:id=\"@+id/divider2\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"1dp\"\n          android:layout_marginTop=\"20dp\"\n          android:background=\"?android:attr/listDivider\" />\n\n        <TextView\n          android:id=\"@+id/user_alias_text_view\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_marginTop=\"20dp\"\n          android:text=\"User Alias\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n          android:id=\"@+id/com_appboy_sample_set_alias_outlined_box\"\n          style=\"@style/DroidboyOutlinedBox\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_weight=\"1\"\n          android:hint=\"Enter Alias\">\n\n            <com.google.android.material.textfield.TextInputEditText\n              android:id=\"@+id/com_appboy_sample_set_alias_edit_text\"\n              style=\"@style/BaseWidget.DroidboyTextInput\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <TextView\n          android:id=\"@+id/user_alias_label_text_view\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_marginTop=\"20dp\"\n          android:text=\"User Alias Label\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n          android:id=\"@+id/com_appboy_sample_set_alias_label_outlined_text_box\"\n          style=\"@style/DroidboyOutlinedBox\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_weight=\"1\"\n          android:hint=\"Enter Alias Label\">\n\n            <com.google.android.material.textfield.TextInputEditText\n              android:id=\"@+id/com_appboy_sample_set_alias_label_edit_text\"\n              android:layout_width=\"match_parent\"\n              style=\"@style/BaseWidget.DroidboyTextInput\"\n              android:layout_height=\"match_parent\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <Button\n          android:id=\"@+id/com_appboy_sample_set_user_alias_button\"\n          style=\"@style/DroidboyOutlinedButton\"\n          android:layout_width=\"200dp\"\n          android:layout_marginTop=\"20dp\"\n          android:text=\"Add User Alias\" />\n\n        <View\n          android:id=\"@+id/divider3\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"1dp\"\n          android:layout_marginTop=\"15dp\"\n          android:layout_marginBottom=\"15dp\"\n          android:background=\"?android:attr/listDivider\" />\n\n        <Button\n          android:id=\"@+id/com_appboy_sample_collect_and_flush_google_advertising_id_button\"\n          style=\"@style/DroidboyOutlinedButton\"\n          android:layout_width=\"250dp\"\n          android:paddingTop=\"10dp\"\n          android:text=\"Get/Flush Google Advertising ID\" />\n\n        <View\n          android:id=\"@+id/divider4\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"1dp\"\n          android:layout_marginTop=\"15dp\"\n          android:layout_marginBottom=\"15dp\"\n          android:background=\"?android:attr/listDivider\" />\n\n        <TextView\n          android:id=\"@+id/sdk_auth_signature_textview\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_marginTop=\"15dp\"\n          android:ems=\"10\"\n          android:text=\"SDK Authorization Signature\"\n          android:fontFamily=\"@font/sailec_bold\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n          android:id=\"@+id/com_appboy_sample_set_sdk_auth_signature_outlined_text_box\"\n          style=\"@style/DroidboyOutlinedBox\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"match_parent\"\n          android:hint=\"SDK Auth Signature\">\n\n            <com.google.android.material.textfield.TextInputEditText\n              android:id=\"@+id/com_appboy_sample_set_sdk_auth_signature_edit_text\"\n              style=\"@style/BaseWidget.DroidboyTextInput\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:inputType=\"textMultiLine\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <Button\n          android:id=\"@+id/com_appboy_sample_set_sdk_auth_signature_button\"\n          style=\"@style/DroidboyOutlinedButton\"\n          android:layout_width=\"200dp\"\n          android:layout_marginTop=\"15dp\"\n          android:text=\"Set SDK Auth Signature\" />\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/preference_wrapper_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:layout_width=\"fill_parent\"\n              android:layout_height=\"fill_parent\"\n              android:orientation=\"vertical\">\n\n  <androidx.appcompat.widget.Toolbar\n      android:id=\"@+id/toolbar\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"?attr/actionBarSize\"\n      android:background=\"?attr/colorPrimary\"\n      android:theme=\"@style/Base.Theme.Droidboy\"/>\n  <ListView android:id=\"@android:id/list\"\n      android:layout_width=\"fill_parent\"\n      android:layout_height=\"fill_parent\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/property_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                xmlns:tools=\"http://schemas.android.com/tools\"\n                android:orientation=\"horizontal\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                tools:ignore=\"RtlHardcoded\">\n\n    <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?android:attr/textAppearanceMedium\"\n            android:text=\"Key\"\n            android:id=\"@+id/key_text_view\"\n            android:layout_toLeftOf=\"@+id/space\"/>\n    <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?android:attr/textAppearanceMedium\"\n            android:text=\" - \"\n            android:id=\"@+id/space\"\n            android:layout_centerHorizontal=\"true\"/>\n    <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?android:attr/textAppearanceMedium\"\n            android:text=\"Value\"\n            android:id=\"@+id/value_text_view\"\n            android:layout_toRightOf=\"@+id/space\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/push_tester.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/com_appboy_push_tester_root\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:ignore=\"RtlHardcoded\">\n  <LinearLayout\n      android:id=\"@+id/push_testing_buttons\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_alignParentBottom=\"true\"\n      style=\"@style/BaseWidget.PushBottomButton\">\n    <Button\n        android:id=\"@+id/test_push_button\"\n        android:text=\"@string/test_push\"\n        style=\"@style/BaseWidget.DroidboyButton\"/>\n  </LinearLayout>\n  <ScrollView\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:layout_alignParentTop=\"true\"\n      android:fillViewport=\"true\"\n      android:layout_above=\"@id/push_testing_buttons\">\n      <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginBottom=\"25dp\"\n        android:orientation=\"vertical\">\n        <CheckBox\n          android:id=\"@+id/push_tester_big_title\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_big_title_label\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_inline_image_push_enabled\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_inline_image_push\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_conversational_push_enabled\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/conversation_push\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_big_summary\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_big_summary_label\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_summary\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_summary_label\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_overflow_text\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_overflow_text_label\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_set_public_version\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_public_version_label\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_test_triggers\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_test_triggers\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_constant_nid\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/push_tester_constant_nid\"/>\n        <CheckBox\n          android:id=\"@+id/push_tester_set_open_webview\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/set_open_uri_in_webview\"/>\n        <CheckBox\n            android:id=\"@+id/push_tester_story_deep_link\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/story_toggle_deep_link\"/>\n        <CheckBox\n            android:id=\"@+id/push_tester_story_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/story_toggle_title\"/>\n        <CheckBox\n            android:id=\"@+id/push_tester_story_subtitle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/story_toggle_subtitle\"/>\n        <CheckBox\n            android:id=\"@+id/push_tester_html\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/set_html_push_text\"/>\n        <Spinner\n          android:id=\"@+id/push_image_spinner\"\n          android:layout_width=\"fill_parent\"\n          android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_image_number_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_story_title_align_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_story_subtitle_align_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n          android:id=\"@+id/push_priority_spinner\"\n          android:layout_width=\"fill_parent\"\n          android:layout_height=\"wrap_content\"/>\n        <Spinner\n          android:id=\"@+id/push_click_action_spinner\"\n          android:layout_width=\"fill_parent\"\n          android:layout_height=\"wrap_content\"/>\n        <Spinner\n          android:id=\"@+id/push_category_spinner\"\n          android:layout_width=\"fill_parent\"\n          android:layout_height=\"wrap_content\"/>\n        <Spinner\n          android:id=\"@+id/push_visibility_spinner\"\n          android:layout_width=\"fill_parent\"\n          android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_action_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_accent_color_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_large_icon_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_notification_factory_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n        <Spinner\n            android:id=\"@+id/push_channel_spinner\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"wrap_content\"/>\n      </LinearLayout>\n  </ScrollView>\n</RelativeLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/set_environment_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/stored_api_key_scrollview\"\n  android:layout_width=\"wrap_content\"\n  android:layout_height=\"wrap_content\">\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n    <EditText\n      android:id=\"@+id/set_environment_override_api_key_alias\"\n      android:layout_width=\"fill_parent\"\n      android:layout_height=\"wrap_content\"\n      android:hint=\"@string/set_environment_override_api_key_alias_hint\"\n      android:inputType=\"text\" />\n    <EditText\n      android:id=\"@+id/set_environment_override_api_key\"\n      android:layout_width=\"fill_parent\"\n      android:layout_height=\"wrap_content\"\n      android:hint=\"@string/set_environment_override_api_key_hint\"\n      android:inputType=\"text\" />\n    <EditText\n      android:id=\"@+id/set_environment_override_endpoint_url\"\n      android:layout_width=\"fill_parent\"\n      android:layout_height=\"wrap_content\"\n      android:hint=\"@string/set_environment_override_endpoint_url_hint\"\n      android:inputType=\"textUri\" />\n    <LinearLayout\n      android:id=\"@+id/stored_api_key_layout\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:orientation=\"vertical\"></LinearLayout>\n    <TextView\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"@string/set_environment_override_api_key_description\" />\n    <TextView\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"@string/set_environment_override_endpoint_description\" />\n    <TextView\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"@string/set_environment_override_prev_api_key_description\" />\n    <include layout=\"@layout/dialog_footer_navigation\" />\n  </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/settings_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"fill_parent\"\n  android:layout_height=\"fill_parent\"\n  android:orientation=\"vertical\">\n  <androidx.appcompat.widget.Toolbar\n    android:id=\"@+id/toolbar\"\n    android:background=\"?attr/colorPrimary\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?attr/actionBarSize\"/>\n  <androidx.fragment.app.FragmentContainerView\n    android:id=\"@+id/settingsFragmentContainer\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\" />\n</LinearLayout>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/spinner_dropdown_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<CheckedTextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  style=\"@style/BaseWidget.SpinnerItem\"\n  android:singleLine=\"true\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:padding=\"5dp\"\n  android:ellipsize=\"marquee\"/>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/spinner_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@android:id/text1\"\n  style=\"@style/BaseWidget.SpinnerItem\"/>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/subscription_state_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"fill_parent\"\n  android:layout_height=\"fill_parent\">\n  <TableLayout\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"4.0dp\"\n    android:paddingRight=\"4.0dp\"\n    android:stretchColumns=\"*\">\n    <TableRow\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\">\n      <RadioGroup\n        android:id=\"@+id/subscription_state\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\">\n        <RadioButton\n          android:id=\"@+id/subscribed\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/subscribed\" />\n        <RadioButton\n          android:id=\"@+id/opted_in\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/opted_in\" />\n        <RadioButton\n          android:id=\"@+id/unsubscribed\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/unsubscribed\" />\n      </RadioGroup>\n    </TableRow>\n    <include layout=\"@layout/dialog_footer_navigation\" />\n  </TableLayout>\n</ScrollView>\n"
  },
  {
    "path": "droidboy/src/main/res/layout/user_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            android:layout_width=\"fill_parent\"\n            android:layout_height=\"fill_parent\">\n\n    <TableLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:paddingLeft=\"4.0dp\"\n        android:paddingRight=\"4.0dp\"\n        android:stretchColumns=\"*\">\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <Button\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"0.5\"\n                android:textSize=\"10sp\"\n                android:id=\"@+id/user_dialog_button_clear\"\n                android:textColor=\"@android:color/black\"\n                android:text=\"@string/user_dialog_clear\"/>\n\n            <Button\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"0.5\"\n                android:textSize=\"10sp\"\n                android:id=\"@+id/user_dialog_button_populate\"\n                android:textColor=\"@android:color/black\"\n                android:text=\"@string/user_dialog_populate\"/>\n        </TableRow>\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <Button\n                android:id=\"@+id/first_name_button\"\n                style=\"@style/BaseWidget.PopulateButton\"\n                android:text=\"@string/user_dialog_first_name\"/>\n\n            <EditText\n                android:id=\"@+id/first_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_span=\"3\"\n                style=\"@style/BaseWidget.UserSingleLineText\"/>\n        </TableRow>\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <Button\n                android:id=\"@+id/last_name_button\"\n                style=\"@style/BaseWidget.PopulateButton\"\n                android:text=\"@string/user_dialog_last_name\"/>\n\n            <EditText\n                android:id=\"@+id/last_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_span=\"3\"\n                style=\"@style/BaseWidget.UserSingleLineText\"/>\n        </TableRow>\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <Button\n                android:id=\"@+id/email_button\"\n                style=\"@style/BaseWidget.PopulateButton\"\n                android:text=\"@string/user_dialog_email_address\"/>\n\n            <EditText\n                android:id=\"@+id/email\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_span=\"3\"\n                style=\"@style/BaseWidget.UserEmailAddress\"/>\n        </TableRow>\n\n        <TableRow\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n            <Button\n                    android:id=\"@+id/birthday_button\"\n                    style=\"@style/BaseWidget.PopulateButton\"\n                    android:text=\"@string/user_dialog_birthday_button\"/>\n\n            <TextView\n                    android:id=\"@+id/birthday\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_span=\"3\"/>\n        </TableRow>\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/user_dialog_gender\"/>\n\n            <RadioGroup\n                android:id=\"@+id/gender\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\">\n\n                <RadioButton\n                    android:id=\"@+id/unspecified\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/unspecified\"/>\n\n                <RadioButton\n                    android:id=\"@+id/male\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/male\"/>\n\n                <RadioButton\n                    android:id=\"@+id/female\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/female\"/>\n\n                <RadioButton\n                    android:id=\"@+id/other\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/other\"/>\n\n                <RadioButton\n                    android:id=\"@+id/unknown\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/unknown\"/>\n\n                <RadioButton\n                    android:id=\"@+id/not_applicable\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/not_applicable\"/>\n\n                <RadioButton\n                    android:id=\"@+id/prefer_not_to_say\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/prefer_not_to_say\"/>\n            </RadioGroup>\n        </TableRow>\n\n        <TableRow\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <Button\n                android:id=\"@+id/language_button\"\n                style=\"@style/BaseWidget.PopulateButton\"\n                android:text=\"@string/user_dialog_language\"/>\n\n            <EditText\n                android:id=\"@+id/language\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_span=\"3\"\n                style=\"@style/BaseWidget.UserSingleLineText\"/>\n        </TableRow>\n      <include layout=\"@layout/dialog_footer_navigation\" />\n    </TableLayout>\n</ScrollView>\n"
  },
  {
    "path": "droidboy/src/main/res/menu/actionbar_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n      android:id=\"@+id/action_settings\"\n      android:title=\"@string/settings\"\n      app:showAsAction=\"ifRoom\" />\n    <item\n      android:id=\"@+id/action_flush\"\n      android:title=\"@string/data_flush_title\"\n      app:showAsAction=\"never\" />\n    <item\n      android:id=\"@+id/feed_categories\"\n      android:title=\"Feed Categories\"\n      app:showAsAction=\"never\" />\n    <item\n      android:id=\"@+id/geofences_map\"\n      android:title=\"Geofences\"\n      app:showAsAction=\"never\" />\n    <item\n      android:id=\"@+id/iam_sandbox\"\n      android:title=\"IAM Sandbox\"\n      app:showAsAction=\"never\" />\n    <item\n      android:id=\"@+id/feed_activity_launch\"\n      android:title=\"NewsFeed\"\n      app:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "droidboy/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- General configuration -->\n  <!-- The API key is not shown to the user and thus we don't require that it be translated. -->\n  <string translatable=\"false\" name=\"com_braze_api_key\">braze-sample-android</string>\n\n  <!-- On GPRS and EDGE, allow a flush request up to every 30 seconds. (default is 60)  -->\n  <integer name=\"com_braze_data_flush_interval_bad_network\">30</integer>\n  <!-- With a 3G connection, allow a flush request up to every 15 seconds. (default is 30) -->\n  <integer name=\"com_braze_data_flush_interval_good_network\">15</integer>\n  <!-- If we're on 4G or Wifi, allow a flush request up to every 10 seconds. (default is 10) -->\n  <integer name=\"com_braze_data_flush_interval_great_network\">10</integer>\n\n  <!-- The length of time before a session times out in seconds. The session manager will \"re-open\"\n  otherwise closed sessions if the call to StartSession comes within this interval. (default is 10) -->\n  <integer name=\"com_braze_session_timeout\">10</integer>\n\n  <!-- Push Notification configuration -->\n  <bool name=\"com_braze_firebase_cloud_messaging_registration_enabled\">true</bool>\n  <string translatable=\"false\" name=\"com_braze_firebase_cloud_messaging_sender_id\">901477453852</string>\n  <bool name=\"com_braze_push_adm_messaging_registration_enabled\">true</bool>\n\n  <!-- This integer sets the default accent color for push notifications on Android Lollipop and higher. If not specified,\n    the default background color is gray (the same gray Lollipop uses for system notifications). This value can\n    be overridden at runtime by a push notification that sets a custom accent color. -->\n  <integer name=\"com_braze_default_notification_accent_color\">0xFF000000</integer>\n\n  <!-- A color resource id can also be set for the accent color as demonstrated below -->\n  <!--  <color name=\"com_braze_default_notification_accent_color\">@color/my_color_here</color>-->\n\n  <drawable name=\"com_braze_push_small_notification_icon\">@drawable/notification_small_icon</drawable>\n  <drawable name=\"com_braze_push_large_notification_icon\">@drawable/notification_large_icon</drawable>\n\n  <!-- News Feed configuration -->\n  <!-- This boolean sets whether the read/unread visual indicator at the top right of news feed cards\n    on or off. -->\n  <bool name=\"com_braze_newsfeed_unread_visual_indicator_on\">true</bool>\n\n  <!-- Location configuration -->\n  <!-- This boolean sets whether Braze should automatically collect location (if the user permits).\n    If set to true, location data will be collected once at the start of each session.\n    If not specified or set to false, all location collection will be disabled and location will\n    never be set for the user unless integrating apps manually call setLastKnownLocation on the\n    BrazeUser. -->\n  <bool name=\"com_braze_enable_location_collection\">true</bool>\n\n  <!-- Triggered Action configuration -->\n  <!-- The minimum interval in seconds between actions, such as an in-app message, that can be triggered\n    by an in-app event, such as the logging of a custom event. This is a general rate limit for\n    actions to prevent overloading a user with messages. Defaults to 30s. The minimum valid value is 0s. -->\n  <integer name=\"com_braze_trigger_action_minimum_time_interval_seconds\">5</integer>\n\n  <!--Handle Deep Links Automatically-->\n  <bool name=\"com_braze_handle_push_deep_links_automatically\">true</bool>\n\n  <bool name=\"com_braze_push_notification_html_rendering_enabled\">true</bool>\n\n  <string-array name=\"com_braze_internal_sdk_metadata\">\n   <item>GRADLE</item>\n  </string-array>\n\n  <string-array name=\"com_braze_sdk_metadata\">\n   <item>UNITY_PACKAGE_MANAGER</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"custom_inappmessage_green\">#FF89E4DF</color>\n  <color name=\"window_background\">@android:color/white</color>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n  <dimen name=\"design_fab_image_size\" tools:override=\"true\">36dp</dimen>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_align_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_align_options\">\n    <item>Not Set</item>\n    <item>Start</item>\n    <item>End</item>\n    <item>Center</item>\n  </string-array>\n  <string-array name=\"inapp_align_values\">\n    <item>@string/not_set</item>\n    <item>start</item>\n    <item>end</item>\n    <item>center</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_boolean_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_boolean_options\">\n    <item>Not Set</item>\n    <item>True</item>\n    <item>False</item>\n  </string-array>\n  <string-array name=\"inapp_boolean_values\">\n    <item>@string/not_set</item>\n    <item>true</item>\n    <item>false</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_button_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_button_options\">\n    <item>Message Buttons: Not Set</item>\n    <item>Message Buttons: None</item>\n    <item>Message Buttons: One - News Feed</item>\n    <item>Message Buttons: One - Long Title</item>\n    <item>Message Buttons: Two - Web</item>\n    <item>Message Buttons: Two - Deep Links</item>\n    <item>Message Buttons: Two - Long Titles</item>\n    <item>Message Buttons: One - Show Push Prompt</item>\n  </string-array>\n  <string-array name=\"inapp_button_values\">\n    <item>@string/not_set</item>\n    <item>@string/none</item>\n    <item>one</item>\n    <item>one_long</item>\n    <item>two</item>\n    <item>deeplink</item>\n    <item>long</item>\n    <item>push_prompt_one</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_click_action_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_click_action_options\">\n    <item>Click Action: Not Set</item>\n    <item>Click Action: Newsfeed</item>\n    <item>Click Action: URI</item>\n    <item>Click Action: None</item>\n  </string-array>\n  <string-array name=\"inapp_click_action_values\">\n    <item>@string/not_set</item>\n    <item>newsfeed</item>\n    <item>uri</item>\n    <item>@string/none</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_color_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_color_options\">\n    <item>Not Set</item>\n    <item>Red</item>\n    <item>Orange</item>\n    <item>Yellow</item>\n    <item>Green</item>\n    <item>Blue</item>\n    <item>Purple</item>\n    <item>Brown</item>\n    <item>Grey</item>\n    <item>Black</item>\n    <item>White</item>\n    <item>Transparent</item>\n  </string-array>\n  <string-array name=\"inapp_color_values\">\n    <item>@string/not_set</item>\n    <item>red</item>\n    <item>orange</item>\n    <item>yellow</item>\n    <item>green</item>\n    <item>blue</item>\n    <item>purple</item>\n    <item>brown</item>\n    <item>grey</item>\n    <item>black</item>\n    <item>white</item>\n    <item>transparent</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_dismiss_type_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_dismiss_type_options\">\n    <item>Dismiss Type: Not Set</item>\n    <item>Dismiss Type: Auto</item>\n    <item>Dismiss Type: Auto Short</item>\n    <item>Dismiss Type: Manual</item>\n  </string-array>\n  <string-array name=\"inapp_dismiss_type_values\">\n    <item>@string/not_set</item>\n    <item>auto</item>\n    <item>auto-short</item>\n    <item>manual</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_frame_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_frame_options\">\n    <item>Frame Color: Not Set</item>\n    <item>Frame Color: Transparent</item>\n    <item>Frame Color: Opaque Blue</item>\n    <item>Frame Color: Almost Transparent Blue</item>\n  </string-array>\n  <string-array name=\"inapp_frame_values\">\n    <item>not set</item>\n    <item>transparent</item>\n    <item>blue</item>\n    <item>almost_transparent_blue</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_header_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_header_options\">\n    <item>Header: Not Set</item>\n    <item>Header: None</item>\n    <item>Header: 10 Chars</item>\n    <item>Header: 20 Chars</item>\n    <item>Header: 30 Chars</item>\n    <item>Header: 80 Chars</item>\n  </string-array>\n  <string-array name=\"inapp_header_values\">\n      <item>@string/not_set</item>\n      <item>@string/none</item>\n      <item>@string/header_10</item>\n      <item>@string/header_20</item>\n      <item>@string/header_30</item>\n      <item>@string/header_80</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_icon_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_icon_options\">\n    <item>Icon: Not Set</item>\n    <item>Icon: None</item>\n    <item>Icon: Horn</item>\n    <item>Icon: Money</item>\n    <item>Icon: Play</item>\n    <item>Icon: Star</item>\n    <item>Icon: Trophy</item>\n    <item>Icon: Video</item>\n    <item>Icon: Connect</item>\n    <item>Icon: Visa</item>\n  </string-array>\n  <string-array name=\"inapp_icon_values\">\n    <item>@string/not_set</item>\n    <item>@string/none</item>\n    <item></item>\n    <item></item>\n    <item></item>\n    <item></item>\n    <item></item>\n    <item></item>\n    <item></item>\n    <item></item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_image_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_image_options\">\n    <item>Image: Not Set</item>\n    <item>Image: None</item>\n    <item>Image: [png] Grid 1160x400 (modal)</item>\n    <item>Image: [png] Grid 1000x1000 (graphic modal)</item>\n    <item>Image: [png] Grid 1000x800 (full - portrait)</item>\n    <item>Image: [png] Grid 1600x500 (full - landscape)</item>\n    <item>Image: [png] Grid 1000x1600 (graphic full - portrait)</item>\n    <item>Image: [png] Grid 1600x1000 (graphic full - landscape)</item>\n    <item>Image: [png] s3</item>\n    <item>Image: [jpg] Blue</item>\n    <item>Image: [gif] Computer Cat</item>\n    <item>Image: [gif] Curious Cat</item>\n    <item>Image: [gif] s3</item>\n  </string-array>\n  <string-array name=\"inapp_image_values\">\n    <item>@string/not_set</item>\n    <item>@string/none</item>\n    <item>@string/appboy_image_url_1160w_400h</item>\n    <item>@string/appboy_image_url_1000w_1000h</item>\n    <item>@string/appboy_image_url_1000w_800h</item>\n    <item>@string/appboy_image_url_1600w_500h</item>\n    <item>@string/appboy_image_url_1000w_1600h</item>\n    <item>@string/appboy_image_url_1600w_1000h</item>\n    <item>@string/appboy_image_url_s3</item>\n    <item>@string/blue_url</item>\n    <item>@string/cat_computer_gif_url</item>\n    <item>@string/cat_scanning_gif_url</item>\n    <item>@string/appboy_gif_url_s3</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_message_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"inapp_message_options\">\n        <item>Message: Not Set</item>\n        <item>Message: 20 Chars</item>\n        <item>Message: 40 Chars</item>\n        <item>Message: 70 Chars</item>\n        <item>Message: 90 Chars</item>\n        <item>Message: 140 Chars</item>\n        <item>Message: 240 Chars</item>\n        <item>Message: 640 Chars</item>\n        <item>Message: 2400 Chars</item>\n    </string-array>\n    <string-array name=\"inapp_message_values\">\n        <item>@string/not_set</item>\n        <item>@string/message_20</item>\n        <item>@string/message_40</item>\n        <item>@string/message_70</item>\n        <item>@string/message_90</item>\n        <item>@string/message_140</item>\n        <item>@string/message_240</item>\n        <item>@string/message_640</item>\n        <item>@string/message_2400</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_message_type_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_message_type_options\">\n    <item>Message Type: Slideup</item>\n    <item>Message Type: Modal</item>\n    <item>Message Type: Modal - Graphic</item>\n    <item>Message Type: Dark Theme Modal</item>\n    <item>Message Type: Full</item>\n    <item>Message Type: Full - Graphic</item>\n    <item>Message Type: HTML Bootstrap Album</item>\n    <item>Message Type: HTML Baby Shark</item>\n    <item>Message Type: HTML Full No JS</item>\n    <item>Message Type: HTML Full Inline JS</item>\n    <item>Message Type: HTML Full External JS</item>\n    <item>Message Type: HTML Full Star Wars</item>\n    <item>Message Type: HTML Full Youtube</item>\n    <item>Message Type: HTML Full Braze Bridge Tester</item>\n    <item>Message Type: HTML Dark Mode</item>\n    <item>Message Type: HTML Slow Loading</item>\n  </string-array>\n  <string-array name=\"inapp_message_type_values\">\n    <item>slideup</item>\n    <item>modal</item>\n    <item>modal_graphic</item>\n    <item>modal_dark_theme</item>\n    <item>full</item>\n    <item>full_graphic</item>\n    <item>html_full_unified_bootstrap</item>\n    <item>html_shark_unified</item>\n    <item>html_full_no_js</item>\n    <item>html_full_inline_js</item>\n    <item>html_full_external_js</item>\n    <item>html_full_star_wars</item>\n    <item>html_full_youtube</item>\n    <item>html_full_bridge_tester</item>\n    <item>html_full_dark_mode</item>\n    <item>html_full_slow_loading</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_orientation_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_orientation_options\">\n    <item>Orientation: Not Set</item>\n    <item>Orientation: Any</item>\n    <item>Orientation: Portrait Only</item>\n    <item>Orientation: Landscape Only</item>\n  </string-array>\n  <string-array name=\"inapp_orientation_values\">\n    <item>not set</item>\n    <item>any</item>\n    <item>portrait</item>\n    <item>landscape</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_slide_from_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"inapp_slide_from_options\">\n    <item>Slide From: Not Set</item>\n    <item>Slide From: Top</item>\n    <item>Slide From: Bottom</item>\n  </string-array>\n  <string-array name=\"inapp_slide_from_values\">\n    <item>@string/not_set</item>\n    <item>top</item>\n    <item>bottom</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/inapp_uri_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n<string-array name=\"inapp_uri_options\">\n  <item>URI: Not Set</item>\n  <item>URI: HTTPS Link</item>\n  <item>URI: Telephone Deep Link</item>\n  <item>URI: Droidboy Deep Link</item>\n  <item>URI: Droidboy Deep Link HTTPS</item>\n  <item>URI: Youtube Link</item>\n</string-array>\n<string-array name=\"inapp_uri_values\">\n  <item>@string/not_set</item>\n  <item>@string/braze_homepage_url</item>\n  <item>@string/telephone_uri</item>\n  <item>@string/droidboy_deep_link</item>\n  <item>@string/droidboy_deep_link_https</item>\n  <item>@string/youtube_https_link</item>\n</string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_accent_color_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_accent_color_options\">\n    <item>Accent Color: Not Set</item>\n    <item>Accent Color: Black</item>\n    <item>Accent Color: White</item>\n    <item>Accent Color: Blue</item>\n    <item>Accent Color: Transparent</item>\n  </string-array>\n  <string-array name=\"push_accent_color_values\">\n    <item></item>\n    <item>0xFF000000</item>\n    <item>0xFFFFFFFF</item>\n    <item>0xFF0073d5</item>\n    <item>0x00000000</item>\n  </string-array>\n</resources>"
  },
  {
    "path": "droidboy/src/main/res/values/push_action_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_action_options\">\n    <item>Action Buttons: Not Set</item>\n    <item>Action Buttons: Open</item>\n    <item>Action Buttons: Http Links</item>\n    <item>Action Buttons: Deep Links</item>\n  </string-array>\n  <string-array name=\"push_action_values\">\n    <item></item>\n    <item>ab_open</item>\n    <item>ab_uri</item>\n    <item>deep_link</item>\n  </string-array>\n</resources>"
  },
  {
    "path": "droidboy/src/main/res/values/push_category_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_category_options\">\n    <item>Category: Not Set</item>\n    <item>Category: Invalid Category</item>\n    <item>Category: Alarm</item>\n    <item>Category: Progress</item>\n    <item>Category: Social</item>\n    <item>Category: Error</item>\n    <item>Category: Transport</item>\n    <item>Category: System</item>\n    <item>Category: Service</item>\n    <item>Category: Recommendation</item>\n    <item>Category: Status</item>\n  </string-array>\n  <string-array name=\"push_category_values\">\n    <item></item>\n    <item>bad_category</item>\n    <item>alarm</item>\n    <item>progress</item>\n    <item>social</item>\n    <item>err</item>\n    <item>transport</item>\n    <item>sys</item>\n    <item>service</item>\n    <item>recommendation</item>\n    <item>status</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_channel_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"push_channel_options\">\n        <item>Notification Channel: Not Set</item>\n        <item>Notification Channel: Messages</item>\n        <item>Notification Channel: Matches</item>\n        <item>Notification Channel: Offers</item>\n        <item>Notification Channel: Recommendations</item>\n    </string-array>\n    <string-array name=\"push_channel_values\">\n        <item></item>\n        <item>@string/droidboy_notification_channel_01_id</item>\n        <item>@string/droidboy_notification_channel_02_id</item>\n        <item>@string/droidboy_notification_channel_03_id</item>\n        <item>@string/droidboy_notification_channel_04_id</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_click_action_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n<string-array name=\"push_click_action_options\">\n  <item>Click Action: Not Set</item>\n  <item>Click Action: Http Link</item>\n  <item>Click Action: Telephone Deep Link</item>\n  <item>Click Action: Droidboy Deep Link Custom Scheme</item>\n  <item>Click Action: Droidboy Deep Link Https</item>\n  <item>Click Action: Firebase Dynamic Link</item>\n  <item>Click Action: Branch Link Param 1</item>\n  <item>Click Action: Branch Link Param 2</item>\n</string-array>\n<string-array name=\"push_click_action_values\">\n  <item></item>\n  <item>@string/braze_homepage_url</item>\n  <item>@string/telephone_uri</item>\n  <item>@string/droidboy_deep_link</item>\n  <item>@string/droidboy_deep_link_https</item>\n  <item>@string/firebase_dynamic_link</item>\n  <item>@string/branch_link_p1</item>\n  <item>@string/branch_link_p2</item>\n</string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_image_number_spinner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_image_number_options\">\n    <item>Story Images: 6</item>\n    <item>Story Images: 2</item>\n    <item>Story Images: 3</item>\n    <item>Story Images: 4</item>\n    <item>Story Images: 5</item>\n    <item>Story Images: 7</item>\n    <item>Story Images: 8</item>\n  </string-array>\n  <string-array name=\"push_image_number_values\">\n    <item>6</item>\n    <item>2</item>\n    <item>3</item>\n    <item>4</item>\n    <item>5</item>\n    <item>7</item>\n    <item>8</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_image_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_image_options\">\n    <item>Push Type: Not Set (Default)</item>\n    <item>Push Type: Image (Appboy)</item>\n    <item>Push Type: Image (s3)</item>\n    <item>Push Type: Image (Blue)</item>\n    <item>Push Type: Push Story</item>\n    <item>Push Type: Random 2:1</item>\n    <item>Push Type: Random 3:2</item>\n  </string-array>\n  <string-array name=\"push_image_values\">\n    <item></item>\n    <item>@string/appboy_image_url</item>\n    <item>@string/appboy_image_url_s3</item>\n    <item>@string/blue_url</item>\n    <item>@string/push_story</item>\n    <item>@string/random_2_by_1_image_url</item>\n    <item>@string/random_3_by_2_image_url</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_large_icon_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_large_icon_options\">\n    <item>Large Icon: Not Set</item>\n    <item>Large Icon: Appboy</item>\n    <item>Large Icon: s3</item>\n    <item>Large Icon: Blue</item>\n  </string-array>\n  <string-array name=\"push_large_icon_values\">\n    <item></item>\n    <item>@string/appboy_image_url</item>\n    <item>@string/appboy_image_url_s3</item>\n    <item>@string/blue_url</item>\n  </string-array>\n</resources>"
  },
  {
    "path": "droidboy/src/main/res/values/push_notification_factory_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"push_notification_factory_options\">\n        <item>Notification Factory: Not Set</item>\n        <item>Notification Factory: Droidboy</item>\n        <item>Notification Factory: Fully Custom</item>\n    </string-array>\n    <string-array name=\"push_notification_factory_values\">\n        <item></item>\n        <item>DroidboyNotificationFactory</item>\n        <item>FullyCustomNotificationFactory</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_priority_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_priority_options\">\n    <item>Priority: Default</item>\n    <item>Priority: Min</item>\n    <item>Priority: Low</item>\n    <item>Priority: High</item>\n    <item>Priority: Max</item>\n  </string-array>\n  <string-array name=\"push_priority_values\">\n    <item>-0</item>\n    <item>-2</item>\n    <item>-1</item>\n    <item>1</item>\n    <item>2</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_story_subtitle_align_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_story_subtitle_align_options\">\n    <item>Story Subtitle Alignment: Not Set</item>\n    <item>Story Subtitle Alignment: Start</item>\n    <item>Story Subtitle Alignment: Center</item>\n    <item>Story Subtitle Alignment: End</item>\n  </string-array>\n  <string-array name=\"push_story_subtitle_align_values\">\n    <item></item>\n    <item>start</item>\n    <item>center</item>\n    <item>end</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_story_title_align_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_story_title_align_options\">\n    <item>Story Title Alignment: Not Set</item>\n    <item>Story Title Alignment: Start</item>\n    <item>Story Title Alignment: Center</item>\n    <item>Story Title Alignment: End</item>\n  </string-array>\n  <string-array name=\"push_story_title_align_values\">\n    <item></item>\n    <item>start</item>\n    <item>center</item>\n    <item>end</item>\n  </string-array>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/push_visibility_options.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string-array name=\"push_visibility_options\">\n    <item>Visibility: Not Set</item>\n    <item>Visibility: Public</item>\n    <item>Visibility: Private</item>\n    <item>Visbility: Secret</item>\n  </string-array>\n  <string-array name=\"push_visibility_values\">\n    <item></item>\n    <item>1</item>\n    <item>0</item>\n    <item>-1</item>\n  </string-array>\n</resources>"
  },
  {
    "path": "droidboy/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"ExtraTranslation\">\n  <string name=\"app_name\">Droidboy</string>\n  <string name=\"feed\">News Feed</string>\n  <string name=\"inappmessage\">In-App Messages</string>\n  <string name=\"settings\">Settings</string>\n  <string name=\"network_control_title\">Network Controls</string>\n  <string name=\"session_title\">Sessions</string>\n  <string name=\"location_title\">Location</string>\n  <string name=\"about_title\">About</string>\n  <string name=\"sdk_state_change_settings_title\">Braze SDK State Change</string>\n  <string name=\"sdk_auth\">SDK Auth</string>\n  <string name=\"user_dialog_title\">Default Attributes</string>\n  <string name=\"user_dialog_okay\">OK</string>\n  <string name=\"user_dialog_cancel\">Cancel</string>\n  <string name=\"user_dialog_close\">Close</string>\n  <string name=\"user_dialog_first_name\">First Name</string>\n  <string name=\"user_dialog_last_name\">Last Name</string>\n  <string name=\"user_dialog_email_address\">Email Address</string>\n  <string name=\"user_dialog_gender\">Gender:</string>\n  <string name=\"user_dialog_language\">Language</string>\n  <string name=\"user_dialog_flush\">Flush on close</string>\n  <string name=\"user_dialog_populate\">Populate</string>\n  <string name=\"user_dialog_clear\">Clear All</string>\n  <string name=\"user_dialog_birthday_button\">Pick Birthday</string>\n  <string name=\"user_dialog_done\">done</string>\n  <string name=\"male\">Male</string>\n  <string name=\"female\">Female</string>\n  <string name=\"other\">Other</string>\n  <string name=\"unknown\">Unknown</string>\n  <string name=\"not_applicable\">Not Applicable</string>\n  <string name=\"prefer_not_to_say\">Prefer not to Say</string>\n  <string name=\"unspecified\">Unspecified</string>\n  <string name=\"subscribed\">Subscribed</string>\n  <string name=\"opted_in\">Opted-In</string>\n  <string name=\"unsubscribed\">Unsubscribed</string>\n  <string name=\"facebook_share_title\">Facebook</string>\n  <string name=\"facebook_share_summary\">Share us on Facebook</string>\n  <string name=\"facebook_share_toast\">Thank you for sharing on Facebook</string>\n  <string name=\"twitter_share_title\">Twitter</string>\n  <string name=\"twitter_share_summary\">Share us on Twitter</string>\n  <string name=\"twitter_share_toast\">Thank you for sharing on Twitter</string>\n  <string name=\"custom_logging_dialog_title\">Custom Logging</string>\n  <string name=\"custom_logging_dialog_summary\">Log User Attributes, Custom Events, and Purchases</string>\n  <string name=\"custom_logging_dialog_okay\">OK</string>\n  <string name=\"custom_logging_dialog_cancel\">Cancel</string>\n  <string name=\"custom_logging_category_title\">Custom Logging</string>\n  <string name=\"log_user_attributes_title\">Custom Attributes</string>\n  <string name=\"log_events_title\">Custom Events</string>\n  <string name=\"log_purchase_title\">Purchases</string>\n  <string name=\"storage_info_preference_category\">Storage Info</string>\n  <string name=\"file_data_title\">File Data</string>\n  <string name=\"file_data_summary\">View the files stored for this app.</string>\n  <string name=\"shared_preferences_title\">SharedPreferences Data</string>\n  <string name=\"shared_preferences_summary\">View the SharedPreferences data for this app.</string>\n  <string name=\"attribute_array\">Array Operation:</string>\n  <string name=\"attribute_array_set\">Set</string>\n  <string name=\"attribute_array_add\">Add</string>\n  <string name=\"attribute_array_remove\">Remove</string>\n  <string name=\"attribute_array_key\">Create Attribute Array</string>\n  <string name=\"custom_logging_user_attribute\">User Attribute</string>\n  <string name=\"custom_logging_user_number_attribute\">User Attribute (Number)</string>\n  <string name=\"custom_logging_user_attribute_increment\">Increment Attribute</string>\n  <string name=\"custom_logging_user_attribute_unset\">Unset Attribute</string>\n  <string name=\"custom_logging_custom_purchase_qty\">Purchase Quantity</string>\n  <string name=\"custom_logging_custom_purchase_currency_code\">Currency Code</string>\n  <string name=\"custom_logging_custom_purchase_price_code\">Price</string>\n  <string name=\"data_flush_title\">Request Flush</string>\n  <string name=\"data_flush_toast\">Data flush requested</string>\n  <string name=\"close_session_toast\">Closed existing Braze session.</string>\n  <string name=\"enabled_outbound_network_requests_toast\">Enabled Braze outbound network requests</string>\n  <string name=\"disabled_outbound_network_requests_toast\">Disabled Braze outbound network requests</string>\n  <string name=\"spinner_content_description\">Spinner</string>\n  <string name=\"spin\">Spin!</string>\n  <string name=\"red\">Red</string>\n  <string name=\"blue\">Blue</string>\n  <string name=\"inappmessage_tester_tab_title\">IAMs</string>\n  <string name=\"create_and_add_inappmessage\">Create and Add In-App Message</string>\n  <string name=\"display_next_inappmessage\">Display Next In-App Message</string>\n  <string name=\"hide_current_inappmessage\">Hide Current In-App Message</string>\n  <string name=\"custom_iappboy_navigator\">Custom IBrazeDeeplinkHandler</string>\n  <string name=\"custom_iappboy_animation\">Custom IInAppMessageAnimationFactory</string>\n  <string name=\"custom_iinappmessagemanager_listener\">Custom IInAppMessageManagerListener</string>\n  <string name=\"custom_html_inappmessage_action_listener\">Custom IHtmlInAppMessageActionListener</string>\n  <string name=\"custom_inappmessage_modal_size\">Custom Max Graphic Modal Size</string>\n  <string name=\"custom_inappmessage_image_radius\">Custom Image Radius (remove radius)</string>\n  <string name=\"disable_back_button_dismiss_behavior\">Disable back button IAM dismissal</string>\n  <string name=\"enable_tap_outside_modal_dismiss_behavior\">Enable tapping outside Modal to dismiss</string>\n  <string name=\"inappmessage_activity\">In-App Messages</string>\n  <string name=\"custom_iinappmessage_view_factory\">Custom IInAppMessageViewFactory</string>\n  <string name=\"push_testing\">Push</string>\n  <string name=\"set_header_align\">Set header align</string>\n  <string name=\"set_message_align\">Set message align</string>\n  <string name=\"set_background_color\">Set background color</string>\n  <string name=\"set_icon_color\">Set icon color</string>\n  <string name=\"set_icon_background_color\">Set icon background color</string>\n  <string name=\"set_close_button_color\">Set close button color</string>\n  <string name=\"set_text_color\">Set text color</string>\n  <string name=\"set_header_text_color\">Set header text color</string>\n  <string name=\"set_button_color\">Set button color</string>\n  <string name=\"set_button_border_color\">Set button border color</string>\n  <string name=\"set_button_text_color\">Set button text color</string>\n  <string name=\"set_animate_in\">Set animate in</string>\n  <string name=\"set_animate_out\">Set animate out</string>\n  <string name=\"set_open_uri_in_webview\">Open Web Links In WebView</string>\n  <string name=\"story_toggle_deep_link\">Toggle Story Deep Link</string>\n  <string name=\"story_toggle_title\">Toggle Story Title</string>\n  <string name=\"story_toggle_subtitle\">Toggle Story Subtitle</string>\n  <string name=\"set_html_push_text\">Test HTML Push</string>\n  <!--'#' characters at the end of headers and messages are used to verify that the entire message was displayed.-->\n  <string name=\"header_10\">Hey there#</string>\n  <string name=\"header_20\">Hey hey from Braze!#</string>\n  <string name=\"header_30\">Good morning from us @ Braze!#</string>\n  <string name=\"header_80\">Hello from Braze!! Have a fun day okay. Hello from Braze!! Have a fun day today#</string>\n  <string name=\"message_20\">Hey hey from Braze!#</string>\n  <string name=\"message_40\">Hello there!  This is an in-app message#</string>\n  <string name=\"message_70\">Hello there! This is an in-app message. Yo, this is an in-app message#</string>\n  <string name=\"message_90\">Hello there! This is an in-app message.  Hello again!  Anyways, this is an in-app message#</string>\n  <string name=\"message_140\">Welcome to Braze!!! Braze is Marketing Automation for Apps. This is an in-app message - this message is exactly one hundred and forty chars#</string>\n  <string name=\"message_240\">Welcome to Braze!!! Braze is Marketing Automation for Apps. This is an in-app message - this message is exactly two hundred and forty chars!  We don\\'t recommend making in-app messages longer than 140 characters due to variations in screens#</string>\n  <string name=\"message_640\">Welcome to Braze!!! Braze is Marketing Automation for Apps. This is an in-app message - this message is exactly six hundred and forty chars!  We don\\'t recommend making in-app messages longer than 140 characters due to variations in screens.  This is an in-app message - this message is exactly six hundred and forty chars!  We don\\'t recommend making in-app messages longer than 140 characters due to variations in screens.  This is an in-app message - this message is exactly six hundred and forty chars!  We don\\'t recommend making in-app messages longer than 140 characters due to variations in screens.  This is a waaaay too long message#</string>\n  <string name=\"message_2400\">Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens. Welcome to Braze! Braze is Marketing Automation for Apps. This is an in-app message, this message is about two thousand four hundred chars! We don\\'t recommend making in-app messages longer than 2400 characters due to variations in screens.#</string>\n  <string name=\"test_push\">Send Push</string>\n  <string name=\"push_test_large_icon_label\">Use Dynamic Large Icon</string>\n  <string name=\"push_tester_big_title_label\">Add Big Title</string>\n  <string name=\"push_tester_inline_image_push\" translatable=\"false\">Inline Image Push</string>\n  <string name=\"push_tester_big_summary_label\">Add Big Summary</string>\n  <string name=\"push_tester_summary_label\">Add Summary Text</string>\n  <string name=\"push_tester_overflow_text_label\">Overflow Text</string>\n  <string name=\"push_tester_public_version_label\">Set Public Version Notification</string>\n  <string name=\"push_tester_test_triggers\">Force request triggers on click</string>\n  <string name=\"push_tester_constant_nid\">Use constant notification Id</string>\n  <string name=\"disable_appboy_network_requests\">Toggle Disable Braze Network Requests</string>\n  <string name=\"disable_appboy_network_requests_summary\">Disable Braze network requests for selected emulators starting next app run</string>\n  <string name=\"push_preference_category\">Push</string>\n  <string name=\"misc_preference_category\">Misc</string>\n  <string name=\"image_loading_preference_category\">Image Display</string>\n  <string name=\"content_card_preference_category\">Content Cards</string>\n  <string name=\"disable_appboy_logging_title\">Toggle Braze Logging</string>\n  <string name=\"disable_appboy_logging_summary\">Toggle Braze Logging.  This does not persist through app runs.</string>\n  <string name=\"get_registration_id_title\">Get Push Registration Id</string>\n  <string name=\"set_email_subscription_state_title\">Set Email Subscription State</string>\n  <string name=\"set_push_subscription_state_title\">Set Push Subscription State</string>\n  <string name=\"sort_feed\">Sort News Feed</string>\n  <string name=\"min_trigger_interval\">Trigger Action Minimum Time Interval (s)</string>\n  <string name=\"custom_news_feed_card_click_action_listener\">Custom INewsFeedClickActionListener</string>\n  <string name=\"get_registration_id_summary\">Retrieve the Registration Id used for push on this device.</string>\n  <string name=\"set_push_subscription_state_summary\">Subscribe, opt-in, or opt-out for push.</string>\n  <string name=\"set_email_subscription_state_summary\">Subscribe, opt-in, or opt-out for email.</string>\n  <string name=\"log_attribution_title\">Log Attribution For User</string>\n  <string name=\"log_attribution_summary\">Log Creative, AdGroup, Network, and Campaign.</string>\n  <string name=\"sort_feed_summary\">Sort the News Feed Cards to Prefer to Display Unread Cards</string>\n  <string name=\"custom_news_feed_card_click_action_listener_summary\">toggles the use of CustomNewsFeedClickActionListener</string>\n  <string name=\"notch_settings_category\">Notch Settings</string>\n  <string name=\"droidboy_notification_group_01_id\">my_group_01</string>\n  <string name=\"droidboy_notification_group_01_name\">Group 1</string>\n  <string name=\"droidboy_notification_channel_01_id\">db_channel_01</string>\n  <string name=\"droidboy_notification_channel_02_id\">db_channel_02</string>\n  <string name=\"droidboy_notification_channel_03_id\">db_channel_03</string>\n  <string name=\"droidboy_notification_channel_04_id\">db_channel_04</string>\n  <string name=\"droidboy_notification_channel_messages_name\">Messages</string>\n  <string name=\"droidboy_notification_channel_messages_desc\">Private messages from your matches</string>\n  <string name=\"droidboy_notification_channel_matches_name\">Matches</string>\n  <string name=\"droidboy_notification_channel_matches_desc\">New matches we\\'ve found for you - don\\'t delay in reaching out!</string>\n  <string name=\"droidboy_notification_channel_offers_name\">Promotions</string>\n  <string name=\"droidboy_notification_channel_offers_desc\">Droidboy offers and exciting Droidboy news</string>\n  <string name=\"droidboy_notification_channel_recommendations_name\">Recommendations</string>\n  <string name=\"droidboy_notification_channel_recommendations_desc\">Recommended suitors, hand-picked just for you</string>\n  <string name=\"appboy_image_url\">https://blog.appboy.com/wp-content/uploads/2014/11/Install_Attribution_Cover.png</string>\n  <string name=\"appboy_image_url_1600w_1000h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c761151b0e4d207934d109/original.?1472684309</string>\n  <string name=\"appboy_image_url_1000w_1000h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c760fb1b0e4d414534d0e8/original.?1472684283</string>\n  <string name=\"appboy_image_url_1000w_800h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c760ef56ec310d03f55668/original.?1472684271</string>\n  <string name=\"appboy_image_url_1000w_1600h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c761025c5a686cbd72479f/original.?1472684290</string>\n  <string name=\"appboy_image_url_1600w_500h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c7610e56ec313c63f556cd/original.?1472684302</string>\n  <string name=\"appboy_image_url_1160w_400h\">https://appboy-images.com/appboy/communication/assets/image_assets/images/57c7610856ec31384ef556bf/original.?1472684296</string>\n  <string name=\"appboy_image_url_s3\">https://s3.amazonaws.com/appboy-staging-dashboard-uploads/appboy/communication/marketing/slide_up/slide_up_message_parameters/images/569934e8bf5cea1409000027/639cf60262df21a9d190c659e2fc36823fa2dc94/original.?1452881133</string>\n  <string name=\"blue_url\">https://s3.amazonaws.com/appboy-staging-test/android-sdk-image-push-integration-test3.jpg</string>\n  <string name=\"cat_computer_gif_url\">https://media.giphy.com/media/VbnUQpnihPSIgIXuZv/giphy.gif</string>\n  <string name=\"cat_scanning_gif_url\">https://media.giphy.com/media/ODy29v7YAJrck/giphy.gif</string>\n  <string name=\"appboy_gif_url_s3\">https://s3.amazonaws.com/appboy-staging-dashboard-uploads/appboy/communication/marketing/slide_up/slide_up_message_parameters/images/564cfde56461737ff4080000/0283e284156a896114fc231e918551b959e87a4e/original.?1452806650</string>\n  <string name=\"braze_homepage_url\">https://www.braze.com</string>\n  <string name=\"google_url\">http://www.google.com</string>\n  <string name=\"droidboy_deep_link\">droidboy://preferences</string>\n  <string name=\"droidboy_deep_link_https\">https://www.droidboy.com/preferences</string>\n  <string name=\"firebase_dynamic_link\">https://rkt5u.app.goo.gl/TPlw</string>\n  <string name=\"branch_link_p1\">https://droid.app.link/preferences</string>\n  <string name=\"branch_link_p2\">https://droid.app.link/tYyoN3LRnB</string>\n  <string name=\"youtube_https_link\">https://www.youtube.com/watch?v=1PJCQTGbLq4</string>\n  <string name=\"telephone_uri\">tel:555555555</string>\n  <string name=\"push_story\">story1</string>\n  <string name=\"random_2_by_1_image_url\">random_2_by_1_image_url</string>\n  <string name=\"random_3_by_2_image_url\">random_3_by_2_image_url</string>\n  <string name=\"play_store_uri\">market://details?id=com.google.android.gm</string>\n  <string name=\"overflow_string\"> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 201 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 202</string>\n  <string name=\"youtube_regex\">^(http\\\\:\\\\/\\\\/|https\\\\:\\\\/\\\\/)?(www\\\\.youtube\\\\.com|youtu\\\\.?be)\\\\/(.+$|)</string>\n  <string name=\"droidboy_close_button_text\">close</string>\n  <string name=\"droidboy_custom_action\">droidboy_custom_action</string>\n  <string name=\"droidboy_icon_name\">ic_stat_notify_droidboy</string>\n  <string name=\"close_icon_name\">com_appboy_ic_close_white</string>\n  <string name=\"close_icon_name_1\">com_appboy_ic_highlight_off_white</string>\n  <string name=\"close_icon_name_2\">com_appboy_ic_cancel_white</string>\n  <string name=\"heart_icon_name\">com_appboy_ic_favorite_white</string>\n  <string name=\"settings_icon_name\">com_appboy_ic_settings_white</string>\n  <string name=\"star_icon_name\">com_appboy_ic_grade_white</string>\n  <string name=\"share_icon_name\">com_appboy_ic_share_white</string>\n  <string name=\"exit_to_app_icon_name\">com_appboy_ic_exit_to_app_white</string>\n  <string name=\"world_icon_name\">com_appboy_ic_language_white</string>\n  <string name=\"none\">none</string>\n  <string name=\"not_set\">not set</string>\n  <string name=\"custom_logging_name\">Name</string>\n  <string name=\"current_log_level\">Current Log Level</string>\n  <string name=\"log_level_dialog_title\">Log Level</string>\n  <string name=\"log_level_pref_summary\">Configure Logger Settings</string>\n  <string name=\"set_environment_title\">Set Environment</string>\n  <string name=\"set_environment_summary\">Override API key and Endpoint</string>\n  <string name=\"set_environment_override_api_key_alias_hint\">API Key Description (empty sets default)</string>\n  <string name=\"set_environment_override_api_key_hint\">API Key (empty sets default)</string>\n  <string name=\"set_environment_override_endpoint_url_hint\">Endpoint (empty sets default)</string>\n  <string name=\"set_environment_override_api_key_description\">Set API Key:</string>\n  <string name=\"set_environment_override_prev_api_key_description\">Previously Used API Keys:</string>\n  <string name=\"set_environment_override_endpoint_description\">Set Endpoint:</string>\n  <string name=\"shared_prefs_location\">com.appboy.sample.sharedpreferences</string>\n  <string name=\"api_key_shared_prefs_location\">com.appboy.sample.sharedpreferences.api_key</string>\n  <string name=\"mock_appboy_network_requests\">mock_appboy_network_requests</string>\n  <string name=\"source_key\">source</string>\n  <string name=\"destination_view\">destination</string>\n  <string name=\"home\">home</string>\n  <string name=\"feed_key\">feed</string>\n  <string name=\"environment_preference_category\">Environment</string>\n  <string name=\"display_message\">Display Message</string>\n  <string name=\"dummy_button\">Dummy Button</string>\n  <string name=\"droidboy_required_location_prompt_title\">Please enable Droidboy location permissions</string>\n  <string name=\"droidboy_required_location_prompt_message\">Droidboy collects location data to enable Braze Geofences and session start location tracking even when the app is closed or not in use. This \"prominent disclosure\" is required by Google for Droidboy and does not automatically appear in our client apps as part of the SDK.</string>\n  <string name=\"droidboy_required_bg_location_prompt_title\">Please enable Droidboy background locations</string>\n  <string name=\"droidboy_required_bg_location_prompt_message\">To enable Geofencing, please allow access all the time.</string>\n  <string name=\"html_push_title_text\">&lt;p&gt;&lt;span style=\\\"color: #99cc00;\\\"&gt;M&lt;/span&gt;u&lt;span style=\\\"color: #008080;\\\"&gt;lti&lt;/span&gt;Colo&lt;span style=\\\"color: #ff6600;\\\"&gt;r&lt;/span&gt; &lt;span style=\\\"color: #000080;\\\"&gt;P&lt;/span&gt;&lt;span style=\\\"color: #00ccff;\\\"&gt;u&lt;/span&gt;&lt;span style=\\\"color: #ff0000;\\\"&gt;s&lt;/span&gt;&lt;span style=\\\"color: #808080;\\\"&gt;h&lt;/span&gt;&lt;/p&gt;</string>\n  <string name=\"html_push_body_text\">&lt;p&gt;&lt;em&gt;test&lt;/em&gt; &lt;span style=\\\"text-decoration: underline; background-color: #ff6600;\\\"&gt;&lt;strong&gt;message&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;</string>\n  <!-- Preference Titles -->\n  <string name=\"messages_header\">Messages</string>\n  <string name=\"sync_header\">Sync</string>\n  <!-- Messages Preferences -->\n  <string name=\"signature_title\">Your signature</string>\n  <string name=\"reply_title\">Default reply action</string>\n  <!-- Sync Preferences -->\n  <string name=\"sync_title\">Sync email periodically</string>\n  <string name=\"attachment_title\">Download incoming attachments</string>\n  <string name=\"attachment_summary_on\">Automatically download attachments for incoming emails</string>\n  <string name=\"attachment_summary_off\">Only download attachments when manually requested</string>\n  <string name=\"settings_fragment_tab_title\">Settings️</string>\n  <string name=\"conversation_push\">Conversation Push</string>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"BaseWidget\">\n        <item name=\"android:fontFamily\">@font/sailec_font_family</item>\n        <item name=\"android:textAllCaps\">false</item>\n    </style>\n\n    <style name=\"BaseWidget.PopulateButton\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:padding\">0dp</item>\n        <item name=\"android:textSize\">10sp</item>\n        <item name=\"android:maxLines\">3</item>\n    </style>\n\n    <style name=\"BaseWidget.DroidboyButton\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:padding\">0dp</item>\n        <item name=\"android:textSize\">12sp</item>\n    </style>\n\n    <style name=\"DroidboyOutlinedButton\" parent=\"Widget.MaterialComponents.Button.OutlinedButton\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:padding\">0dp</item>\n        <item name=\"font\">@font/sailec_bold</item>\n        <item name=\"android:textSize\">12sp</item>\n        <item name=\"android:textAllCaps\">false</item>\n        <item name=\"backgroundTint\">@android:color/white</item>\n        <item name=\"android:textColor\">#43cdcc</item>\n        <item name=\"strokeColor\">#43cdcc</item>\n    </style>\n\n    <style name=\"BaseWidget.DroidboyTextInput\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"font\">@font/sailec_regular</item>\n        <item name=\"android:textColorHint\">@android:color/darker_gray</item>\n        <item name=\"boxStrokeColor\">@android:color/darker_gray</item>\n    </style>\n\n    <style name=\"DroidboyOutlinedBox\" parent=\"Widget.MaterialComponents.TextInputLayout.OutlinedBox\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_gravity\">start</item>\n        <item name=\"android:textColorHint\">@android:color/darker_gray</item>\n        <item name=\"android:textStyle\">italic</item>\n        <item name=\"font\">@font/sailec_regular</item>\n        <item name=\"boxStrokeColor\">@android:color/darker_gray</item>\n    </style>\n\n\n    <style name=\"BaseWidget.UserSingleLineText\">\n        <item name=\"android:inputType\">text|textCapWords</item>\n        <item name=\"android:singleLine\">true</item>\n    </style>\n\n    <style name=\"BaseWidget.UserEmailAddress\">\n        <item name=\"android:inputType\">textEmailAddress</item>\n        <item name=\"android:singleLine\">true</item>\n    </style>\n\n    <style name=\"BaseWidget.InAppMessageTesterButton\">\n        <item name=\"android:layout_width\">0.0dp</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:textSize\">13sp</item>\n    </style>\n\n    <style name=\"BaseWidget.InAppMessageTesterCheckBox\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n    </style>\n\n    <style name=\"BaseWidget.InAppMessageTesterTextView\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:paddingRight\">20dp</item>\n        <item name=\"android:textStyle\">bold</item>\n    </style>\n\n    <style name=\"BaseWidget.PushBottomButton\">\n        <item name=\"android:padding\">4dp</item>\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_alignParentBottom\">true</item>\n        <item name=\"android:background\">@color/window_background</item>\n    </style>\n\n    <style name=\"BaseWidget.SpinnerItem\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:singleLine\">true</item>\n        <item name=\"android:ellipsize\">marquee</item>\n    </style>\n\n    <style name=\"Braze.Feed.List\">\n        <item name=\"android:background\">@color/window_background</item>\n        <item name=\"android:cacheColorHint\">@color/window_background</item>\n        <item name=\"android:divider\">@android:color/transparent</item>\n        <item name=\"android:dividerHeight\">16.0dp</item>\n        <item name=\"android:paddingLeft\">12.5dp</item>\n        <item name=\"android:paddingRight\">5.0dp</item>\n        <item name=\"android:scrollbarStyle\">outsideInset</item>\n    </style>\n\n    <style name=\"Theme.Droidboy\" parent=\"Base.Theme.Droidboy\"></style>\n\n    <style name=\"Base.Theme.Droidboy\" parent=\"Theme.MaterialComponents.Light.NoActionBar\">\n        <item name=\"colorPrimary\">#1c222b</item>\n        <item name=\"colorPrimaryDark\">#1c222b</item>\n        <item name=\"colorAccent\">#3accdd</item>\n        <item name=\"android:windowBackground\">@color/window_background</item>\n        <item name=\"titleTextColor\">@android:color/white</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values-sw600dp/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"Braze.Cards.ImageSwitcher\">\n    <item name=\"brazeFeedCustomReadIcon\">@drawable/com_braze_content_card_icon_unread</item>\n    <item name=\"brazeFeedCustomUnReadIcon\">@drawable/com_braze_content_card_icon_read</item>\n    <item name=\"android:layout_width\">30.0dp</item>\n    <item name=\"android:layout_height\">30.0dp</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:gravity\">right|top</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values-v28/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"DisplayInNotchTheme\">\n    <item name=\"android:windowLayoutInDisplayCutoutMode\">\n      shortEdges\n    </item>\n    <item name=\"windowActionBar\">false</item>\n    <item name=\"windowNoTitle\">true</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/values-xlarge/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <style name=\"Braze.Cards.ImageSwitcher\">\n    <item name=\"brazeFeedCustomReadIcon\">@drawable/com_braze_content_card_icon_unread</item>\n    <item name=\"brazeFeedCustomUnReadIcon\">@drawable/com_braze_content_card_icon_read</item>\n    <item name=\"android:layout_width\">30.0dp</item>\n    <item name=\"android:layout_height\">30.0dp</item>\n    <item name=\"android:layout_alignParentRight\">true</item>\n    <item name=\"android:gravity\">right|top</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "droidboy/src/main/res/xml/preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n  <PreferenceCategory\n    android:key=\"settings_about_key\"\n    android:title=\"@string/about_title\"\n    app:initialExpandedChildrenCount=\"5\">\n    <Preference\n      android:key=\"current_user_id\"\n      android:title=\"Current User ID\" />\n    <Preference\n      android:key=\"sdk_version\"\n      android:title=\"SDK Version\" />\n    <Preference\n      android:key=\"push_token\"\n      android:title=\"Push Token\" />\n    <Preference\n      android:key=\"device_id\"\n      android:title=\"Device Id\" />\n    <Preference\n      android:key=\"build_type\"\n      android:title=\"Build Type\" />\n    <Preference\n      android:key=\"version_code\"\n      android:title=\"Version Code\" />\n    <Preference\n      android:key=\"build_name\"\n      android:title=\"Build Name\" />\n    <Preference\n      android:key=\"install_time\"\n      android:title=\"Install Time\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:title=\"@string/sdk_auth\">\n    <Preference\n      android:key=\"enable_sdk_auth\"\n      android:title=\"Enable SDK Auth\"\n      android:summary=\"Will restart the app\"/>\n    <Preference\n      android:key=\"disable_sdk_auth\"\n      android:title=\"Disable SDK Auth\"\n      android:summary=\"Will restart the app\"/>\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/content_card_preference_category\">\n    <Preference\n      android:key=\"content_card_populate_random_cards_setting_key\"\n      android:summary=\"Does not alter existing saved cards\"\n      android:title=\"Add random cards\" />\n    <Preference\n      android:key=\"content_card_dismiss_all_cards_setting_key\"\n      android:title=\"Dismiss all Cached Content Cards\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/custom_logging_category_title\">\n    <Preference\n      android:key=\"show_user_dialog\"\n      android:title=\"@string/user_dialog_title\" />\n    <Preference\n      android:key=\"show_user_attribute_dialog\"\n      android:title=\"@string/log_user_attributes_title\" />\n    <Preference\n      android:key=\"show_custom_event_dialog\"\n      android:title=\"@string/log_events_title\" />\n    <Preference\n      android:key=\"show_log_purchase_dialog\"\n      android:title=\"@string/log_purchase_title\" />\n    <Preference\n      android:key=\"show_push_subscription_state_dialog\"\n      android:title=\"@string/set_push_subscription_state_title\" />\n    <Preference\n      android:key=\"show_email_subscription_state_dialog\"\n      android:title=\"@string/set_email_subscription_state_title\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/sdk_state_change_settings_title\">\n    <Preference\n      android:key=\"enable_sdk_key\"\n      android:title=\"Re-Enable SDK\" />\n    <Preference\n      android:key=\"disable_sdk_key\"\n      android:title=\"Disable SDK\" />\n    <Preference\n      android:key=\"wipe_data_preference_key\"\n      android:summary=\"Wipes all data from the SDK\"\n      android:title=\"Wipe Data from the SDK\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:key=\"settings_network_control_key\"\n    android:title=\"@string/network_control_title\"\n    app:initialExpandedChildrenCount=\"0\">\n    <Preference\n      android:key=\"data_flush\"\n      android:title=\"@string/data_flush_title\" />\n    <Preference\n      android:key=\"enable_outbound_network_requests\"\n      android:title=\"Re-Enable Outbound Network Requests\" />\n    <Preference\n      android:key=\"disable_outbound_network_requests\"\n      android:title=\"Disable Outbound Network Requests\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/image_loading_preference_category\">\n    <Preference\n      android:key=\"glide_image_loader_enable_setting_key\"\n      android:summary=\"Enables GIFs to display\"\n      android:title=\"Enable Glide Image Library\" />\n    <Preference\n      android:key=\"glide_image_loader_disable_setting_key\"\n      android:summary=\"Disables GIF display\"\n      android:title=\"Disable Glide Image Library\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/environment_preference_category\">\n    <Preference\n      android:key=\"environment_barcode_picture_intent_key\"\n      android:summary=\"Takes a picture of a braze environment barcode\"\n      android:title=\"Set Environment via barcode\" />\n    <Preference\n      android:key=\"environment_reset_key\"\n      android:title=\"Reset environment override\" />\n    <Preference\n      android:key=\"environment_switch_dev\"\n      android:title=\"❄️ Change to Dev endpoint ❄️\" />\n    <Preference\n      android:key=\"show_set_environment_dialog\"\n      android:title=\"@string/set_environment_title\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:key=\"settings_misc_section_key\"\n    android:title=\"@string/misc_preference_category\"\n    app:initialExpandedChildrenCount=\"1\">\n    <Preference\n      android:key=\"logcat_export_file_key\"\n      android:summary=\"Will reset the logcat after click!\"\n      android:title=\"Export Droidboy Logcat to a File\" />\n    <Preference\n      android:key=\"log_attribution\"\n      android:summary=\"@string/log_attribution_summary\"\n      android:title=\"@string/log_attribution_title\" />\n    <Preference\n      android:key=\"anonymous_revert\"\n      android:summary=\"Will cause an app restart.\"\n      android:title=\"Revert to Anonymous User\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/session_title\">\n    <Preference\n      android:key=\"open_session\"\n      android:title=\"Open Session\" />\n    <Preference\n      android:key=\"close_session\"\n      android:title=\"Close Session\" />\n  </PreferenceCategory>\n  <PreferenceCategory android:title=\"@string/location_title\">\n    <Preference\n      android:key=\"set_manual_location\"\n      android:summary=\"Override current location\"\n      android:title=\"Set Manual Location\" />\n    <Preference\n      android:key=\"location_runtime_permission_dialog\"\n      android:summary=\"Request Runtime Permission\"\n      android:title=\"Location Permission Dialog\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:key=\"settings_notch_section_key\"\n    android:title=\"@string/notch_settings_category\"\n    app:initialExpandedChildrenCount=\"0\">\n    <SwitchPreferenceCompat\n      android:defaultValue=\"false\"\n      android:key=\"display_in_full_cutout_setting_key\"\n      android:summary=\"Enables Droidboy to display in the cutout\"\n      android:summaryOff=\"Not displaying content in the cutout\"\n      android:summaryOn=\"Displaying content in the cutout\"\n      android:title=\"Enable Display in Cutout Mode\" />\n    <SwitchPreferenceCompat\n      android:defaultValue=\"false\"\n      android:key=\"display_no_limits_setting_key\"\n      android:summary=\"Enables Droidboy to display with transparent navigation/status bar\"\n      android:summaryOff=\"Not displaying content with no limits\"\n      android:summaryOn=\"Displaying content with no limits\"\n      android:title=\"Enable Display No Limits Mode\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:key=\"settings_feed_section_key\"\n    android:title=\"@string/feed\"\n    app:initialExpandedChildrenCount=\"0\">\n    <SwitchPreferenceCompat\n      android:defaultValue=\"false\"\n      android:key=\"sort_feed\"\n      android:summary=\"@string/sort_feed_summary\"\n      android:title=\"@string/sort_feed\" />\n    <SwitchPreferenceCompat\n      android:defaultValue=\"false\"\n      android:key=\"set_custom_news_feed_card_click_action_listener\"\n      android:summary=\"@string/custom_news_feed_card_click_action_listener_summary\"\n      android:title=\"@string/custom_news_feed_card_click_action_listener\" />\n  </PreferenceCategory>\n  <PreferenceCategory\n    android:key=\"settings_inappmessage_section_key\"\n    android:title=\"@string/inappmessage\">\n      <EditTextPreference\n        android:defaultValue=\"\"\n        android:inputType=\"numberDecimal\"\n        android:digits=\"0123456789\"\n        android:key=\"min_trigger_interval\"\n        app:useSimpleSummaryProvider=\"true\"\n        android:title=\"@string/min_trigger_interval\" />\n      <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"show_own_message_when_inapp_msg_not_triggered\"\n        android:summary=\"Shows our own message when an open, custom event, or purchase does not trigger an in-app message\"\n        android:summaryOff=\"Will not display our own message\"\n        android:summaryOn=\"Will display our own message\"\n        android:title=\"In-App Message Not Triggered Handler\" />\n  </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "droidboy/src/main/res/xml/provider_filepaths.xml",
    "content": "<paths>\n    <cache-path name=\"cache\" path=\"/\" />\n</paths>\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Sep 03 12:42:12 CDT 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.2-all.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Gradle\n# Give the daemon more memory by default\norg.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n\n# Google Play Services\nPLAY_SERVICES_LOCATION_VERSION=21.0.1\nPLAY_SERVICES_MAPS_VERSION=17.0.0\nPLAY_SERVICES_TAG_MANAGER=17.0.0\n\n# AndroidX\nANDROIDX_APPCOMPAT_VERSION=1.4.1\nGOOGLE_MATERIAL_VERSION=1.5.0\nANDROIDX_FRAGMENT_VERSION=1.4.1\nANDROIDX_CORE_VERSION=1.7.0\nANDROIDX_RECYCLERVIEW_VERSION=1.2.1\nANDROIDX_ANNOTATIONS_VERSION=1.2.0\nANDROIDX_SWIPE_REFRESH_LAYOUT_VERSION=1.1.0\nANDROIDX_WEBKIT_VERSION=1.4.0\nANDROIDX_PREFERENCE_VERSION=1.2.0\nANDROIDX_CONSTRAINT_LAYOUT_VERSION=2.1.3\nANDROIDX_MULTIDEX_VERSION=2.0.1\n\n# Grgit Version\nAJOBERSTAR_GIT_GRADLE_PLUGIN_VERSION=4.1.1\n\n# Glide Library\nGLIDE_VERSION=4.11.0\n\n# Firebase\nFIREBASE_PUSH_MESSAGING_VERSION=23.0.0\nFIREBASE_CORE_VERSION=17.5.0\nGOOGLE_ML_VISION_BARCODE=16.1.4\nFIREBASE_CRASHLYTICS_VERSION=18.2.7\n\n# Braze\nBRAZE_SDK_VERSION=24.3.0\n\n# Kotlin\nKOTLIN_VERSION=1.6.0\nKOTLIN_COROUTINES_VERSION=1.6.1\nKOTLIN_LIFECYCLE_RUNTIME_KTX_VERSION=2.4.0\n\nandroid.useAndroidX=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "samples/README.md",
    "content": "![Braze Logo](https://github.com/Appboy/appboy-android-sdk/blob/master/braze-logo.png)\n\n# Samples\n\nTo run a sample app, replace the placeholder value for `com_braze_api_key` in the `src/main/res/values/braze.xml` file of the sample app you wish to run with your API key.\n\nNext, run `./gradlew :samples:<sample-app-name>:installDebug` from the root directory of this repo, replacing `<sample-app-name>` with the project name of the app you'd like to run.\n\nUsage Example:\n`./gradlew :samples:manual-session-integration:installDebug`\n"
  },
  {
    "path": "samples/custom-broadcast/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.custombroadcast\"\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionCode 1\n    versionName \"1.0\"\n\n  }\n  buildTypes {\n    release {\n      minifyEnabled true\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation project(':android-sdk-ui')\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n}\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.braze.custombroadcast\"\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\">\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n\n    <application\n        android:name=\".CustomBroadcastApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity android:name=\".MainActivity\"\n          android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <receiver android:name=\".CustomBroadcastReceiver\" android:exported=\"false\" >\n            <intent-filter>\n                <action android:name=\"com.braze.push.intent.NOTIFICATION_OPENED\" />\n                <action android:name=\"com.braze.push.intent.NOTIFICATION_RECEIVED\" />\n                <action android:name=\"com.braze.push.intent.NOTIFICATION_DELETED\" />\n            </intent-filter>\n        </receiver>\n        <service android:name=\"com.braze.push.BrazeFirebaseMessagingService\"\n          android:exported=\"false\"\n          tools:ignore=\"Instantiatable\">\n            <intent-filter>\n                <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n            </intent-filter>\n        </service>\n    </application>\n</manifest>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/java/com/braze/custombroadcast/CustomBroadcastApplication.java",
    "content": "package com.braze.custombroadcast;\n\nimport android.app.Application;\nimport android.util.Log;\n\nimport com.braze.Braze;\nimport com.braze.BrazeActivityLifecycleCallbackListener;\nimport com.braze.configuration.BrazeConfig;\nimport com.braze.support.BrazeLogger;\n\npublic class CustomBroadcastApplication extends Application {\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    BrazeLogger.setLogLevel(Log.VERBOSE);\n\n    BrazeConfig.Builder appboyConfig = new BrazeConfig.Builder()\n        .setDefaultNotificationChannelName(\"Braze Push\")\n        .setDefaultNotificationChannelDescription(\"Braze related push\");\n    Braze.configure(this, appboyConfig.build());\n\n    registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener());\n  }\n}\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/java/com/braze/custombroadcast/CustomBroadcastReceiver.java",
    "content": "package com.braze.custombroadcast;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.util.Log;\n\nimport com.braze.Constants;\nimport com.braze.push.BrazeNotificationUtils;\nimport com.braze.support.BrazeLogger;\n\nimport java.util.concurrent.TimeUnit;\n\npublic class CustomBroadcastReceiver extends BroadcastReceiver {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(CustomBroadcastReceiver.class);\n\n  @Override\n  public void onReceive(Context context, Intent intent) {\n    String action = intent.getAction();\n    if (action == null) {\n      return;\n    }\n    Log.d(TAG, String.format(\"Received intent with action %s\", action));\n    logNotificationDuration(intent);\n\n    switch (action) {\n      case Constants.BRAZE_PUSH_INTENT_NOTIFICATION_RECEIVED:\n        Log.d(TAG, \"Received push notification.\");\n        break;\n      case Constants.BRAZE_PUSH_INTENT_NOTIFICATION_OPENED:\n        BrazeNotificationUtils.routeUserWithNotificationOpenedIntent(context, intent);\n        break;\n      case Constants.BRAZE_PUSH_INTENT_NOTIFICATION_DELETED:\n        Log.d(TAG, \"Received push notification deleted intent.\");\n        break;\n      default:\n        Log.d(TAG, String.format(\"Ignoring intent with unsupported action %s\", action));\n    }\n  }\n\n  /**\n   * Logs the length of time elapsed since the notification's creation time.\n   */\n  private void logNotificationDuration(Intent intent) {\n    // Log the duration of the push notification\n    Bundle extras = intent.getExtras();\n    if (extras != null && extras.containsKey(Constants.BRAZE_PUSH_RECEIVED_TIMESTAMP_MILLIS)) {\n      long createdAt = extras.getLong(Constants.BRAZE_PUSH_RECEIVED_TIMESTAMP_MILLIS);\n      long durationMillis = System.currentTimeMillis() - createdAt;\n      long durationSeconds = TimeUnit.MILLISECONDS.toSeconds(durationMillis);\n      Log.i(TAG, \"Notification active for \" + durationSeconds + \" seconds.\");\n    }\n  }\n}\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/java/com/braze/custombroadcast/MainActivity.java",
    "content": "package com.braze.custombroadcast;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.braze.Braze;\n\npublic class MainActivity extends AppCompatActivity {\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n    final Context applicationContext = getApplicationContext();\n    final EditText userIdInput = findViewById(R.id.editTextUserId);\n    Button submitUserId = findViewById(R.id.buttonChangeUser);\n\n    submitUserId.setOnClickListener(view -> {\n      String userId = userIdInput.getText().toString();\n      if (userId == null || userId.length() == 0) {\n        showMessage(\"User Id should not be null or empty. Doing nothing.\");\n        return;\n      } else {\n        showMessage(String.format(\"Changed user to %s and requested flush to Braze\", userId));\n        Braze.getInstance(applicationContext).changeUser(userId);\n      }\n\n      Braze.getInstance(applicationContext).requestImmediateDataFlush();\n    });\n  }\n\n  private void showMessage(String message) {\n    Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();\n  }\n}\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/activity_main\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingBottom=\"@dimen/activity_vertical_margin\"\n    android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n    android:paddingRight=\"@dimen/activity_horizontal_margin\"\n    android:paddingTop=\"@dimen/activity_vertical_margin\"\n    tools:context=\"com.appboy.custombroadcast.MainActivity\">\n\n    <EditText\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:ems=\"10\"\n        android:id=\"@+id/editTextUserId\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_alignParentStart=\"true\"\n        android:hint=\"@string/set_user_id_edit_text_hint\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentEnd=\"true\"/>\n\n    <Button\n        android:text=\"@string/set_userid_button_prompt\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:id=\"@+id/buttonChangeUser\"\n        android:layout_below=\"@+id/editTextUserId\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentEnd=\"true\" />\n</RelativeLayout>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- General configuration -->\n  <!-- The API key is not shown to the user and thus we don't require that it be translated. -->\n  <string translatable=\"false\" name=\"com_braze_api_key\">YOUR-BRAZE-API-KEY-HERE</string>\n\n</resources>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string translatable=\"false\" name=\"app_name\">Custom Broadcast</string>\n\n    <string translatable=\"false\" name=\"set_user_id_edit_text_hint\">Set a User ID here</string>\n    <string translatable=\"false\" name=\"set_userid_button_prompt\">Set User ID and request data flush</string>\n</resources>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "samples/custom-broadcast/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/firebase-push/README.md",
    "content": "# Firebase Push Sample\n\nThis sample app showcases our Firebase integration for push.\n\nTo get the sample working, you will need to create a Firebase project, add a `google-services.json` to the root of this sample project, and set your Firebase sender id in `res/values/strings.xml`. The package name is \"com.braze.firebasepush\".\n\nFor more information, see our [firebase push][1] documentation.\n\n[1]: https://www.braze.com/docs/developer_guide/platform_integration_guides/android/push_notifications/integration/\n"
  },
  {
    "path": "samples/firebase-push/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply from: rootProject.file(\"config/buildscript/break-compile-on-deprecations.gradle\")\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.firebasepush\"\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionCode 1\n    versionName \"1.0\"\n  }\n  buildTypes {\n    release {\n      minifyEnabled true\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation project(':android-sdk-ui')\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n  implementation \"com.google.firebase:firebase-messaging:${FIREBASE_PUSH_MESSAGING_VERSION}\"\n}\n\napply plugin: 'com.google.gms.google-services'\n"
  },
  {
    "path": "samples/firebase-push/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"498092073032\",\n    \"firebase_url\": \"https://fir-sample-app-1c7c6.firebaseio.com\",\n    \"project_id\": \"fir-sample-app-1c7c6\",\n    \"storage_bucket\": \"fir-sample-app-1c7c6.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:498092073032:android:0dec0be5e724ad84\",\n        \"android_client_info\": {\n          \"package_name\": \"com.braze.firebasepush\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"498092073032-fffeabukn0usl8iei00rmd5nf9tt1oph.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"\"\n        }\n      ],\n      \"services\": {\n        \"analytics_service\": {\n          \"status\": 1\n        },\n        \"appinvite_service\": {\n          \"status\": 1,\n          \"other_platform_oauth_client\": []\n        },\n        \"ads_service\": {\n          \"status\": 2\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}\n"
  },
  {
    "path": "samples/firebase-push/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          xmlns:tools=\"http://schemas.android.com/tools\"\n          package=\"com.braze.firebasepush\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk tools:overrideLibrary=\"com.google.firebase.messaging\"/>\n\n  <application\n    android:name=\".FirebaseApplication\"\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\">\n    <activity\n      android:name=\".MainActivity\"\n      android:label=\"@string/app_name\"\n      android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n    <service android:name=\"com.braze.push.BrazeFirebaseMessagingService\"\n      android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n      </intent-filter>\n    </service>\n  </application>\n</manifest>\n"
  },
  {
    "path": "samples/firebase-push/src/main/java/com/braze/firebasepush/FirebaseApplication.java",
    "content": "package com.braze.firebasepush;\n\nimport android.app.Application;\nimport android.content.Context;\nimport android.util.Log;\n\nimport com.braze.Braze;\nimport com.braze.BrazeActivityLifecycleCallbackListener;\nimport com.braze.configuration.BrazeConfig;\nimport com.braze.support.BrazeLogger;\nimport com.google.firebase.messaging.FirebaseMessaging;\n\npublic class FirebaseApplication extends Application {\n  private static final String TAG = FirebaseApplication.class.getName();\n\n  @Override\n  public void onCreate() {\n    super.onCreate();\n\n    BrazeConfig.Builder appboyConfig = new BrazeConfig.Builder()\n        .setDefaultNotificationChannelName(\"Braze Push\")\n        .setDefaultNotificationChannelDescription(\"Braze related push\")\n        .setPushDeepLinkBackStackActivityEnabled(true)\n        .setPushDeepLinkBackStackActivityClass(MainActivity.class)\n        .setInAppMessageTestPushEagerDisplayEnabled(true)\n        .setHandlePushDeepLinksAutomatically(true);\n\n    Braze.configure(this, appboyConfig.build());\n\n    registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener());\n    BrazeLogger.setLogLevel(Log.VERBOSE);\n\n    // Example of how to register for Firebase Cloud Messaging manually.\n    final Context applicationContext = this;\n    FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {\n      if (!task.isSuccessful()) {\n        Log.w(TAG, \"Fetching FCM registration token failed\", task.getException());\n        return;\n      }\n\n      final String token = task.getResult();\n      Log.i(TAG, \"================\");\n      Log.i(TAG, \"================\");\n      Log.i(TAG, \"Registering firebase token in Application class: \" + token);\n      Log.i(TAG, \"================\");\n      Log.i(TAG, \"================\");\n      Braze.getInstance(applicationContext).setRegisteredPushToken(token);\n    });\n  }\n}\n"
  },
  {
    "path": "samples/firebase-push/src/main/java/com/braze/firebasepush/MainActivity.java",
    "content": "package com.braze.firebasepush;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.braze.Braze;\n\npublic class MainActivity extends AppCompatActivity {\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n\n    final Context applicationContext = getApplicationContext();\n    final EditText userIdInput = findViewById(R.id.editTextUserId);\n    Button submitUserId = findViewById(R.id.buttonChangeUser);\n\n    submitUserId.setOnClickListener(view -> {\n      String userId = userIdInput.getText().toString();\n      if (userId.length() == 0) {\n        showMessage(\"User Id should not be null or empty. Doing nothing.\");\n        return;\n      } else {\n        showMessage(String.format(\"Changed user to %s and requested flush\", userId));\n        Braze.getInstance(applicationContext).changeUser(userId);\n      }\n\n      Braze.getInstance(applicationContext).requestImmediateDataFlush();\n    });\n  }\n\n  private void showMessage(String message) {\n    Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();\n  }\n}\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/activity_main\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:paddingBottom=\"@dimen/activity_vertical_margin\"\n  android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n  android:paddingRight=\"@dimen/activity_horizontal_margin\"\n  android:paddingTop=\"@dimen/activity_vertical_margin\"\n  tools:context=\"com.appboy.firebasepush.MainActivity\">\n\n  <EditText\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:ems=\"10\"\n    android:id=\"@+id/editTextUserId\"\n    android:layout_alignParentTop=\"true\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentStart=\"true\"\n    android:hint=\"@string/set_user_id_edit_text_hint\"\n    android:layout_alignParentRight=\"true\"\n    android:layout_alignParentEnd=\"true\"/>\n\n  <Button\n    android:text=\"@string/set_userid_button_prompt\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:id=\"@+id/buttonChangeUser\"\n    android:layout_below=\"@+id/editTextUserId\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentStart=\"true\"\n    android:layout_alignParentRight=\"true\"\n    android:layout_alignParentEnd=\"true\" />\n</RelativeLayout>\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string translatable=\"false\" name=\"com_braze_api_key\">YOUR-BRAZE-API-KEY-HERE</string>\n\n</resources>\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#3F51B5</color>\n  <color name=\"colorPrimaryDark\">#303F9F</color>\n  <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/values/dimens.xml",
    "content": "<resources>\n  <!-- Default screen margins, per the Android Design guidelines. -->\n  <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n  <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string translatable=\"false\" name=\"app_name\">Firebase Push</string>\n  <string translatable=\"false\" name=\"droid_boy\">DroidBoyFirebase</string>\n  <string translatable=\"false\" name=\"firebase_register\">Register Firebase Token With Appboy</string>\n  <string translatable=\"false\" name=\"sender_id\">498092073032</string>\n  <string translatable=\"false\" name=\"firebase_scope\">FCM</string>\n  <string translatable=\"false\" name=\"set_userid_button_prompt\">Set User Id and request data flush</string>\n  <string translatable=\"false\" name=\"set_user_id_edit_text_hint\">Set a user id here</string>\n</resources>\n"
  },
  {
    "path": "samples/firebase-push/src/main/res/values/styles.xml",
    "content": "<resources>\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "samples/glide-image-integration/README.md",
    "content": "# Glide Image Integration Sample\n\nThis sample app showcases an example integration with the [Glide Image Library][1].\n\nTo get the sample working, you'll need to set a Braze API Key in the `braze.xml` file.\n\n[1]: https://bumptech.github.io/glide/\n"
  },
  {
    "path": "samples/glide-image-integration/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.glideimageintegration\"\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionCode 1\n    versionName \"1.0\"\n    testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n  }\n  buildTypes {\n    release {\n      minifyEnabled true\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation project(':android-sdk-ui')\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n  implementation \"com.github.bumptech.glide:glide:${GLIDE_VERSION}\"\n}\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.braze.glideimageintegration\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n\n  <application\n    android:name=\".GlideIntegrationApplication\"\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\">\n    <activity android:name=\".MainActivity\"\n      android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n  </application>\n</manifest>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/java/com/braze/glideimageintegration/GlideBrazeImageLoader.java",
    "content": "package com.braze.glideimageintegration;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.os.Bundle;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.braze.models.cards.Card;\nimport com.braze.enums.BrazeViewBounds;\nimport com.braze.images.IBrazeImageLoader;\nimport com.braze.models.inappmessage.IInAppMessage;\nimport com.braze.support.BrazeLogger;\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.request.RequestOptions;\n\npublic class GlideBrazeImageLoader implements IBrazeImageLoader {\n  private static final String TAG = GlideBrazeImageLoader.class.getName();\n\n  private RequestOptions mRequestOptions = new RequestOptions();\n\n  @Override\n  public void renderUrlIntoCardView(@NonNull Context context,\n                                    @Nullable Card card,\n                                    @NonNull String imageUrl,\n                                    @NonNull ImageView imageView,\n                                    @Nullable BrazeViewBounds viewBounds) {\n    renderUrlIntoView(context, imageUrl, imageView, viewBounds);\n  }\n\n  @Override\n  public void renderUrlIntoInAppMessageView(@NonNull Context context,\n                                            @NonNull IInAppMessage inAppMessage,\n                                            @NonNull String imageUrl,\n                                            @NonNull ImageView imageView,\n                                            @NonNull BrazeViewBounds viewBounds) {\n    renderUrlIntoView(context, imageUrl, imageView, viewBounds);\n  }\n\n  @Override\n  public Bitmap getPushBitmapFromUrl(@NonNull Context context,\n                                     @Nullable Bundle extras,\n                                     @NonNull String imageUrl,\n                                     @Nullable BrazeViewBounds viewBounds) {\n    return getBitmapFromUrl(context, imageUrl, viewBounds);\n  }\n\n  @Override\n  public Bitmap getInAppMessageBitmapFromUrl(@NonNull Context context,\n                                             @NonNull IInAppMessage inAppMessage,\n                                             @NonNull String imageUrl,\n                                             @Nullable BrazeViewBounds viewBounds) {\n    return getBitmapFromUrl(context, imageUrl, viewBounds);\n  }\n\n  private void renderUrlIntoView(Context context,\n                                 String imageUrl,\n                                 ImageView imageView,\n                                 BrazeViewBounds viewBounds) {\n    Glide.with(context)\n        .load(imageUrl)\n        .apply(mRequestOptions)\n        .into(imageView);\n  }\n\n  private Bitmap getBitmapFromUrl(Context context,\n                                  String imageUrl,\n                                  BrazeViewBounds viewBounds) {\n    try {\n      return Glide.with(context)\n          .asBitmap()\n          .apply(mRequestOptions)\n          .load(imageUrl).submit().get();\n    } catch (Exception e) {\n      BrazeLogger.e(TAG, \"Failed to retrieve bitmap at url: \" + imageUrl, e);\n    }\n    return null;\n  }\n\n  @Override\n  public void setOffline(boolean isOffline) {\n    // If the loader is offline, then we should only be retrieving from the cache\n    mRequestOptions = mRequestOptions.onlyRetrieveFromCache(isOffline);\n  }\n}\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/java/com/braze/glideimageintegration/GlideIntegrationApplication.java",
    "content": "package com.braze.glideimageintegration;\n\nimport android.app.Application;\nimport android.util.Log;\n\nimport com.braze.Braze;\nimport com.braze.BrazeActivityLifecycleCallbackListener;\nimport com.braze.support.BrazeLogger;\n\npublic class GlideIntegrationApplication extends Application {\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    BrazeLogger.setLogLevel(Log.VERBOSE);\n    registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener());\n    Braze.getInstance(this).setImageLoader(new GlideBrazeImageLoader());\n  }\n}\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/java/com/braze/glideimageintegration/MainActivity.java",
    "content": "package com.braze.glideimageintegration;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.braze.Braze;\nimport com.braze.enums.inappmessage.CropType;\nimport com.braze.enums.inappmessage.DismissType;\nimport com.braze.enums.inappmessage.ImageStyle;\nimport com.braze.models.inappmessage.InAppMessageBase;\nimport com.braze.models.inappmessage.InAppMessageFull;\nimport com.braze.models.inappmessage.InAppMessageModal;\nimport com.braze.models.inappmessage.InAppMessageSlideup;\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager;\n\npublic class MainActivity extends AppCompatActivity {\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.main_activity);\n\n    // Create an in-app message as soon as the Activity starts\n    InAppMessageFull inAppMessageFull = new InAppMessageFull();\n    inAppMessageFull.setImageStyle(ImageStyle.GRAPHIC);\n    inAppMessageFull.setRemoteImageUrl(getString(R.string.gif_3_url));\n    inAppMessageFull.setHeader(\"Glide Sample App\");\n    inAppMessageFull.setMessage(\"It's Amazing\");\n\n    final Context context = this.getApplicationContext();\n    findViewById(R.id.com_appboy_display_modal_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        InAppMessageModal inAppMessageModal = new InAppMessageModal();\n        inAppMessageModal.setRemoteImageUrl(getString(R.string.gif_1_url));\n        inAppMessageModal.setImageStyle(ImageStyle.GRAPHIC);\n        inAppMessageModal.setCropType(CropType.CENTER_CROP);\n        showInAppMessage(inAppMessageModal);\n      }\n    });\n    findViewById(R.id.com_appboy_display_slideup_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        InAppMessageSlideup inAppMessageSlideup = new InAppMessageSlideup();\n        inAppMessageSlideup.setRemoteImageUrl(getString(R.string.gif_2_url));\n        inAppMessageSlideup.setMessage(\"This is a slideup with a GIF\");\n        showInAppMessage(inAppMessageSlideup);\n      }\n    });\n    findViewById(R.id.com_appboy_flush_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        Braze.getInstance(context).requestImmediateDataFlush();\n      }\n    });\n  }\n\n  private void showInAppMessage(InAppMessageBase inAppMessage) {\n    inAppMessage.setDismissType(DismissType.MANUAL);\n    BrazeInAppMessageManager.getInstance().addInAppMessage(inAppMessage);\n  }\n}\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/layout/main_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:orientation=\"vertical\">\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n    <Button\n      android:text=\"Display Modal\"\n      android:id=\"@+id/com_appboy_display_modal_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n    <Button\n      android:text=\"Display Slideup\"\n      android:id=\"@+id/com_appboy_display_slideup_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n    <Button\n      android:text=\"Flush Data\"\n      android:id=\"@+id/com_appboy_flush_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n  </LinearLayout>\n  <fragment android:name=\"com.braze.ui.BrazeFeedFragment\"\n            android:id=\"@+id/com_appboy_feed\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"/>\n</LinearLayout>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- General configuration -->\n  <!-- The API key is not shown to the user and thus we don't require that it be translated. -->\n  <string translatable=\"false\" name=\"com_braze_api_key\">YOUR-BRAZE-API-KEY-HERE</string>\n\n</resources>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string translatable=\"false\" name=\"app_name\">Glide Image Integration</string>\n    <string translatable=\"false\" name=\"gif_1_url\">https://appboy-staging-dashboard-uploads.s3.amazonaws.com/appboy/communication/marketing/slide_up/slide_up_message_parameters/images/5a4e9faafbe76a3365b191ab/e12c1497cbfc5f17ae83f2ad82aa863739f0c1ff/original.gif?1515102126</string>\n    <string translatable=\"false\" name=\"gif_2_url\">https://appboy-staging-dashboard-uploads.s3.amazonaws.com/appboy/communication/marketing/slide_up/slide_up_message_parameters/images/5a4ea091bf5cea7ee0bbb717/e05236f1a0fdf18bad93988b9a0594f75adb2b8e/original.gif?1515102356</string>\n    <string translatable=\"false\" name=\"gif_3_url\">https://appboy-staging-dashboard-uploads.s3.amazonaws.com/appboy/communication/marketing/slide_up/slide_up_message_parameters/images/5a4e961ffbe76a059e5c66ad/d6beae66762f50d2f3f6b34909dff9341a400529/original.gif?1515099684</string>\n</resources>\n"
  },
  {
    "path": "samples/glide-image-integration/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "samples/google-tag-manager/README.md",
    "content": "# Google Tag Manager Sample\n\nThis sample app showcases an example Google Tag Manager custom tag provider.\n\nFor more information, see our Google Tag Manager for Android documentation.\n"
  },
  {
    "path": "samples/google-tag-manager/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.googletagmanager\"\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionCode 1\n    versionName \"1.0\"\n    multiDexEnabled true\n  }\n  buildTypes {\n    release {\n      minifyEnabled false\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  lintOptions {\n    ignore(\"Instantiatable\")\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation(project(':android-sdk-ui')) {\n    exclude group: 'com.google.firebase'\n    exclude group: 'com.google.android.gms'\n  }\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n  implementation \"com.google.android.gms:play-services-tagmanager:${PLAY_SERVICES_TAG_MANAGER}\"\n  implementation \"androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}\"\n}\n\napply plugin: 'com.google.gms.google-services'\n"
  },
  {
    "path": "samples/google-tag-manager/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"170050408521\",\n    \"firebase_url\": \"https://tag-manager-sample-c227d.firebaseio.com\",\n    \"project_id\": \"tag-manager-sample-c227d\",\n    \"storage_bucket\": \"tag-manager-sample-c227d.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:170050408521:android:8a8dfe7c9b7c0bdc306f98\",\n        \"android_client_info\": {\n          \"package_name\": \"com.braze.googletagmanager\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"170050408521-p492ht9psevabuadfu90039i9qvv5bb3.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"170050408521-p492ht9psevabuadfu90039i9qvv5bb3.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.braze.googletagmanager\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n\n  <application\n    android:name=\".GtmApplication\"\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\">\n    <activity\n      android:name=\".MainActivity\"\n      android:label=\"@string/app_name\"\n      android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n    <service android:name=\"com.braze.push.BrazeFirebaseMessagingService\" android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n      </intent-filter>\n    </service>\n  </application>\n</manifest>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/assets/containers/GTM-5HM9ZRX.json",
    "content": "{\n\n\"fingerprint\":\"MTM$0\",\n\n\"resource\": {\n  \"version\":\"13\",\n  \n  \"macros\":[{\n      \"function\":\"__e\",\n      \"instance_name\":\"Event Name\"\n    },{\n      \"function\":\"__md\",\n      \"instance_name\":\"Braze eventName\",\n      \"vtp_setDefaultValue\":false,\n      \"vtp_eventType\":\"CUSTOM\",\n      \"vtp_key\":\"eventName\"\n    },{\n      \"function\":\"__md\",\n      \"instance_name\":\"bool Param\",\n      \"vtp_setDefaultValue\":false,\n      \"vtp_eventType\":\"CUSTOM\",\n      \"vtp_key\":\"boolParam\"\n    },{\n      \"function\":\"__md\",\n      \"instance_name\":\"double Param\",\n      \"vtp_setDefaultValue\":false,\n      \"vtp_eventType\":\"CUSTOM\",\n      \"vtp_key\":\"doubleParam\"\n    },{\n      \"function\":\"__md\",\n      \"instance_name\":\"Int Param\",\n      \"vtp_setDefaultValue\":false,\n      \"vtp_eventType\":\"CUSTOM\",\n      \"vtp_key\":\"intParam\"\n    },{\n      \"function\":\"__md\",\n      \"instance_name\":\"string Param\",\n      \"vtp_setDefaultValue\":false,\n      \"vtp_eventType\":\"CUSTOM\",\n      \"vtp_key\":\"stringParam\"\n    },{\n      \"function\":\"__ai\",\n      \"instance_name\":\"App ID\"\n    },{\n      \"function\":\"__an\",\n      \"instance_name\":\"App Name\"\n    },{\n      \"function\":\"__av\",\n      \"instance_name\":\"App Version Code\"\n    },{\n      \"function\":\"__avn\",\n      \"instance_name\":\"App Version Name\"\n    }],\n  \"tags\":[{\n      \"function\":\"__funct\",\n      \"once_per_event\":true,\n      \"vtp_functionArgument\":[\"list\",[\"map\",\"key\",\"eventName\",\"value\",[\"macro\",1]],[\"map\",\"key\",\"boolParam\",\"value\",[\"macro\",2]],[\"map\",\"key\",\"doubleParam\",\"value\",[\"macro\",3]],[\"map\",\"key\",\"intParam\",\"value\",[\"macro\",4]],[\"map\",\"key\",\"stringParam\",\"value\",[\"macro\",5]],[\"map\",\"key\",\"actionType\",\"value\",\"logEvent\"]],\n      \"vtp_classPath\":\"com.appboy.googletagmanager.BrazeGtmTagProvider\",\n      \"tag_id\":11\n    },{\n      \"function\":\"__funct\",\n      \"once_per_event\":true,\n      \"vtp_functionArgument\":[\"list\",[\"map\",\"key\",\"event_name\",\"value\",\"purchase\"],[\"map\",\"key\",\"prop1\",\"value\",\"1\"],[\"map\",\"key\",\"prop2\",\"value\",\"2\"]],\n      \"vtp_classPath\":\"path.to.Braze\",\n      \"tag_id\":16\n    },{\n      \"function\":\"__funct\",\n      \"once_per_event\":true,\n      \"vtp_functionArgument\":[\"list\",[\"map\",\"key\",\"event_name\",\"value\",\"signup\"],[\"map\",\"key\",\"prop1\",\"value\",\"1\"],[\"map\",\"key\",\"prop2\",\"value\",\"2\"]],\n      \"vtp_classPath\":\"path.to.Braze\",\n      \"tag_id\":17\n    }],\n  \"predicates\":[{\n      \"function\":\"_eq\",\n      \"arg0\":[\"macro\",0],\n      \"arg1\":\"logEvent\"\n    },{\n      \"function\":\"_cn\",\n      \"arg0\":[\"macro\",0],\n      \"arg1\":\"purchase\"\n    },{\n      \"function\":\"_cn\",\n      \"arg0\":[\"macro\",0],\n      \"arg1\":\"signup\"\n    }],\n  \"rules\":[\n    [[\"if\",0],[\"add\",0]],\n    [[\"if\",1],[\"add\",1]],\n    [[\"if\",2],[\"add\",2]]]\n},\n\"runtime\":\n[\n[50,\"__an_main\",[46],[36,[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"applicationName\",[7]]]],[50,\"__an\",[46,\"data\"],[36,[\"__an_main\",[15,\"data\"]]]],[50,\"__ai_main\",[46],[36,[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"applicationId\",[7]]]],[50,\"__ai\",[46,\"data\"],[36,[\"__ai_main\",[15,\"data\"]]]],[50,\"__md_main\",[46,\"a\"],[41,\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\",\"j\",\"k\",\"l\"],[22,[12,[17,[15,\"a\"],\"eventType\"],\"CUSTOM\"],[46,[3,\"b\",[16,[15,\"a\"],\"key\"]]],[46,[22,[12,[17,[15,\"a\"],\"eventType\"],\"SUGGESTED\"],[46,[47,\"c\",[15,\"a\"],[46,[3,\"d\",[2,[15,\"c\"],\"lastIndexOf\",[7,\"Param\"]]],[22,[1,[29,[15,\"d\"],[27,1]],[12,[15,\"d\"],[37,[17,[15,\"c\"],\"length\"],5]]],[46,[22,[20,[15,\"b\"],[44]],[46,[3,\"b\",[16,[15,\"a\"],[15,\"c\"]]]],[46,[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"e\",[0,[0,[0,\"Ignoring unexpected additional parameter \",\"key in the data (key \\u003d \\\"\"],[15,\"c\"]],\"\\\").\"]]]]]]]]]],[46,[22,[20,[17,[15,\"a\"],\"eventType\"],[44]],[46,[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"w\",\"Missing expected eventType param\"]],[3,\"b\",[16,[15,\"a\"],\"key\"]]],[46,[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"e\",[0,\"Unexpected eventType param value: \",[17,[15,\"a\"],\"eventType\"]]]],[36]]]]]]],[22,[20,[15,\"b\"],[44]],[46,[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"e\",\"No parameter key specified in the data.\"]],[36]]],[3,\"e\",[30,[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"eventParameters\",[7]],[8]]],[22,[12,[2,[15,\"b\"],\"indexOf\",[7,\".\"]],[27,1]],[46,[3,\"f\",[16,[15,\"e\"],[15,\"b\"]]],[22,[1,[1,[20,[15,\"f\"],[44]],[12,[17,[15,\"a\"],\"eventType\"],\"SUGGESTED\"]],[17,[15,\"a\"],\"checkEcommerceValue\"]],[46,[3,\"g\",[7,\"items\",\"promotions\"]],[47,\"h\",[15,\"g\"],[46,[3,\"i\",[16,[15,\"g\"],[15,\"h\"]]],[22,[21,[16,[15,\"e\"],[15,\"i\"]],[44]],[46,[3,\"j\",[16,[15,\"e\"],[15,\"i\"]]],[22,[2,[17,[15,\"gtmUtils\"],\"common\"],\"isArray\",[7,[15,\"j\"]]],[46,[22,[21,[16,[16,[15,\"j\"],0],[15,\"b\"]],[44]],[46,[36,[16,[16,[15,\"j\"],0],[15,\"b\"]]]]]],[46,[22,[21,[16,[15,\"j\"],[15,\"b\"]],[44]],[46,[36,[16,[15,\"j\"],[15,\"b\"]]]]]]]]]]]]]],[46,[3,\"k\",[2,[17,[15,\"gtmUtils\"],\"common\"],\"split\",[7,[15,\"b\"],\".\"]]],[3,\"f\",[15,\"e\"]],[47,\"l\",[15,\"k\"],[46,[3,\"f\",[16,[15,\"f\"],[16,[15,\"k\"],[15,\"l\"]]]],[22,[20,[15,\"f\"],[44]],[46,[4]]]]]]],[22,[21,[15,\"f\"],[44]],[46,[36,[15,\"f\"]]],[46,[22,[21,[17,[15,\"a\"],\"defaultValue\"],[44]],[46,[36,[17,[15,\"a\"],\"defaultValue\"]]],[46,[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"w\",[0,[0,\"Event does not have parameter \\\"\",[15,\"b\"]],\"\\\" and no default value was defined. Returning \\\"undefined\\\".\"]]]]]]]],[50,\"__md\",[46,\"data\"],[36,[\"__md_main\",[15,\"data\"]]]],[50,\"__av_main\",[46],[36,[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"applicationVersion\",[7]]]],[50,\"__av\",[46,\"data\"],[36,[\"__av_main\",[15,\"data\"]]]],[50,\"__avn_main\",[46],[36,[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"applicationVersionName\",[7]]]],[50,\"__avn\",[46,\"data\"],[36,[\"__avn_main\",[15,\"data\"]]]],[50,\"__funct_main\",[46,\"a\"],[41,\"b\"],[3,\"b\",[8]],[22,[17,[15,\"a\"],\"functionArgument\"],[46,[3,\"b\",[30,[2,[17,[15,\"gtmUtils\"],\"common\"],\"tableToMap\",[7,[17,[15,\"a\"],\"functionArgument\"],\"key\",\"value\"]],[8]]]]],[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"customTag\",[7,[17,[15,\"a\"],\"classPath\"],[15,\"b\"]]]],[50,\"__funct\",[46,\"data\"],[36,[\"__funct_main\",[15,\"data\"]]]],[50,\"__e_main\",[46],[41,\"a\",\"b\"],[3,\"a\",[2,[17,[15,\"gtmUtils\"],\"mobile\"],\"event\",[7]]],[22,[1,[1,[15,\"a\"],[18,[17,[15,\"a\"],\"length\"],0]],[12,[16,[15,\"a\"],0],\"_\"]],[46,[3,\"b\",[8,\"_f\",\"first_open\",\"_v\",\"first_visit\",\"_iap\",\"in_app_purchase\",\"_e\",\"user_engagement\",\"_s\",\"session_start\",\"_ssr\",\"session_start_with_rollout\",\"_au\",\"app_update\",\"_ui\",\"app_remove\",\"_ab\",\"app_background\",\"_ou\",\"os_update\",\"_cd\",\"app_clear_data\",\"_ae\",\"app_exception\",\"_nf\",\"notification_foreground\",\"_nr\",\"notification_receive\",\"_no\",\"notification_open\",\"_nd\",\"notification_dismiss\",\"_cmp\",\"firebase_campaign\",\"_cmpx\",\"invalid_campaign\",\"_vs\",\"screen_view\",\"_ar\",\"ad_reward\",\"_asr\",\"app_store_refund\",\"_assc\",\"app_store_subscription_convert\",\"_assr\",\"app_store_subscription_renew\",\"_asse\",\"app_store_subscription_cancel\"]],[3,\"a\",[30,[16,[15,\"b\"],[15,\"a\"]],[15,\"a\"]]]]],[36,[15,\"a\"]]],[50,\"__e\",[46,\"data\"],[36,[\"__e_main\",[15,\"data\"]]]],\n[50,\"main\",[46,\"a\"],[43,[17,[15,\"a\"],\"common\"],\"tableToMap\",[15,\"tableToMap\"]],[43,[17,[15,\"a\"],\"common\"],\"stringify\",[15,\"stringify\"]],[43,[17,[15,\"a\"],\"common\"],\"copy\",[15,\"copy\"]],[43,[17,[15,\"a\"],\"common\"],\"split\",[15,\"split\"]]],[50,\"tableToMap\",[46,\"a\",\"b\",\"c\"],[41,\"d\",\"e\",\"f\"],[3,\"d\",[8]],[3,\"e\",false],[3,\"f\",0],[42,[1,[15,\"a\"],[23,[15,\"f\"],[17,[15,\"a\"],\"length\"]]],[33,[15,\"f\"],[3,\"f\",[0,[15,\"f\"],1]]],false,[46,[22,[1,[1,[16,[15,\"a\"],[15,\"f\"]],[2,[16,[15,\"a\"],[15,\"f\"]],\"hasOwnProperty\",[7,[15,\"b\"]]]],[2,[16,[15,\"a\"],[15,\"f\"]],\"hasOwnProperty\",[7,[15,\"c\"]]]],[46,[43,[15,\"d\"],[16,[16,[15,\"a\"],[15,\"f\"]],[15,\"b\"]],[16,[16,[15,\"a\"],[15,\"f\"]],[15,\"c\"]]],[3,\"e\",true]]]]],[36,[39,[15,\"e\"],[15,\"d\"],[45]]]],[50,\"stringify\",[46,\"a\"],[41,\"b\",\"c\",\"d\",\"e\"],[22,[20,[15,\"a\"],[45]],[46,[36,\"null\"]]],[22,[20,[15,\"a\"],[44]],[46,[36,[44]]]],[22,[30,[12,[40,[15,\"a\"]],\"number\"],[12,[40,[15,\"a\"]],\"boolean\"]],[46,[36,[2,[15,\"a\"],\"toString\",[7]]]]],[22,[12,[40,[15,\"a\"]],\"string\"],[46,[36,[0,[0,\"\\\"\",[2,[\"split\",[15,\"a\"],\"\\\"\"],\"join\",[7,\"\\\\\\\"\"]]],\"\\\"\"]]]],[22,[2,[17,[15,\"gtmUtils\"],\"common\"],\"isArray\",[7,[15,\"a\"]]],[46,[3,\"b\",[7]],[3,\"c\",0],[42,[23,[15,\"c\"],[17,[15,\"a\"],\"length\"]],[33,[15,\"c\"],[3,\"c\",[0,[15,\"c\"],1]]],false,[46,[3,\"d\",[\"stringify\",[16,[15,\"a\"],[15,\"c\"]]]],[22,[12,[15,\"d\"],[44]],[46,[2,[15,\"b\"],\"push\",[7,\"null\"]]],[46,[2,[15,\"b\"],\"push\",[7,[15,\"d\"]]]]]]],[36,[0,[0,\"[\",[2,[15,\"b\"],\"join\",[7,\",\"]]],\"]\"]]]],[22,[12,[40,[15,\"a\"]],\"object\"],[46,[3,\"b\",[7]],[47,\"e\",[15,\"a\"],[46,[3,\"d\",[\"stringify\",[16,[15,\"a\"],[15,\"e\"]]]],[22,[29,[15,\"d\"],[44]],[46,[2,[15,\"b\"],\"push\",[7,[0,[0,[0,\"\\\"\",[15,\"e\"]],\"\\\":\"],[15,\"d\"]]]]]]]],[36,[0,[0,\"{\",[2,[15,\"b\"],\"join\",[7,\",\"]]],\"}\"]]]],[2,[17,[15,\"gtmUtils\"],\"common\"],\"log\",[7,\"e\",\"Attempting to stringify unknown type!\"]],[36,[44]]],[50,\"split\",[46,\"a\",\"b\"],[41,\"c\",\"d\",\"e\",\"f\"],[3,\"c\",[7]],[22,[20,[15,\"b\"],\"\"],[46,[3,\"d\",[17,[15,\"a\"],\"length\"]],[3,\"e\",0],[42,[23,[15,\"e\"],[15,\"d\"]],[33,[15,\"e\"],[3,\"e\",[0,[15,\"e\"],1]]],false,[46,[2,[15,\"c\"],\"push\",[7,[16,[15,\"a\"],[15,\"e\"]]]]]],[36,[15,\"c\"]]]],[42,[1,[15,\"a\"],[19,[2,[15,\"a\"],\"indexOf\",[7,[15,\"b\"]]],0]],[46],false,[46,[3,\"f\",[2,[15,\"a\"],\"indexOf\",[7,[15,\"b\"]]]],[22,[12,[15,\"f\"],0],[46,[2,[15,\"c\"],\"push\",[7,\"\"]]],[46,[2,[15,\"c\"],\"push\",[7,[2,[15,\"a\"],\"substring\",[7,0,[15,\"f\"]]]]]]],[3,\"a\",[2,[15,\"a\"],\"substring\",[7,[0,[15,\"f\"],[17,[15,\"b\"],\"length\"]]]]]]],[2,[15,\"c\"],\"push\",[7,[15,\"a\"]]],[36,[15,\"c\"]]],[50,\"copy\",[46,\"a\",\"b\"],[41,\"c\",\"d\"],[3,\"b\",[30,[15,\"b\"],[39,[2,[17,[15,\"gtmUtils\"],\"common\"],\"isArray\",[7,[15,\"a\"]]],[7],[8]]]],[47,\"c\",[15,\"a\"],[46,[3,\"d\",[16,[15,\"a\"],[15,\"c\"]]],[22,[2,[17,[15,\"gtmUtils\"],\"common\"],\"isArray\",[7,[15,\"d\"]]],[46,[22,[28,[2,[17,[15,\"gtmUtils\"],\"common\"],\"isArray\",[7,[16,[15,\"b\"],[15,\"c\"]]]]],[46,[43,[15,\"b\"],[15,\"c\"],[7]]]],[43,[15,\"b\"],[15,\"c\"],[\"copy\",[15,\"d\"],[16,[15,\"b\"],[15,\"c\"]]]]],[46,[22,[1,[29,[15,\"d\"],[45]],[12,[40,[15,\"d\"]],\"object\"]],[46,[22,[29,[40,[16,[15,\"b\"],[15,\"c\"]]],\"object\"],[46,[43,[15,\"b\"],[15,\"c\"],[8]]]],[43,[15,\"b\"],[15,\"c\"],[\"copy\",[15,\"d\"],[16,[15,\"b\"],[15,\"c\"]]]]],[46,[43,[15,\"b\"],[15,\"c\"],[15,\"d\"]]]]]]]],[36,[15,\"b\"]]]\n]\n}\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/java/com/braze/googletagmanager/BrazeGtmTagProvider.java",
    "content": "package com.braze.googletagmanager;\n\nimport android.app.Application;\nimport android.content.Context;\n\nimport com.braze.Braze;\nimport com.braze.BrazeUser;\nimport com.braze.models.outgoing.BrazeProperties;\nimport com.braze.support.BrazeLogger;\nimport com.google.android.gms.tagmanager.CustomTagProvider;\n\nimport java.util.Date;\nimport java.util.Map;\n\npublic class BrazeGtmTagProvider implements CustomTagProvider {\n  private static final String TAG = BrazeLogger.getBrazeLogTag(BrazeGtmTagProvider.class);\n  private static final String ACTION_TYPE_KEY = \"actionType\";\n\n  // Custom Events\n  private static final String LOG_EVENT_ACTION_TYPE = \"logEvent\";\n  private static final String EVENT_NAME_VARIABLE = \"eventName\";\n\n  // Custom Attributes\n  private static final String CUSTOM_ATTRIBUTE_ACTION_TYPE = \"customAttribute\";\n  private static final String CUSTOM_ATTRIBUTE_KEY = \"customAttributeKey\";\n  private static final String CUSTOM_ATTRIBUTE_VALUE_KEY = \"customAttributeValue\";\n\n  // Change User\n  private static final String CHANGE_USER_ACTION_TYPE = \"changeUser\";\n  private static final String CHANGE_USER_ID_VARIABLE = \"externalUserId\";\n\n  private static Context sApplicationContext;\n\n  /**\n   * Must be set before calling any of the below methods to\n   * ensure that the proper application context is available when needed.\n   *\n   * Recommended to be called in your {@link Application#onCreate()}.\n   */\n  public static void setApplicationContext(Context applicationContext) {\n    if (applicationContext != null) {\n      sApplicationContext = applicationContext.getApplicationContext();\n    }\n  }\n\n  @Override\n  public void execute(Map<String, Object> map) {\n    BrazeLogger.i(TAG, \"Got Google Tag Manager parameters map: \" + map);\n\n    if (sApplicationContext == null) {\n      BrazeLogger.w(TAG, \"No application context provided to this tag provider.\");\n      return;\n    }\n\n    if (!map.containsKey(ACTION_TYPE_KEY)) {\n      BrazeLogger.w(TAG, \"Map does not contain the Braze action type key: \" + ACTION_TYPE_KEY);\n      return;\n    }\n    String actionType = String.valueOf(map.remove(ACTION_TYPE_KEY));\n\n    switch (actionType) {\n      case LOG_EVENT_ACTION_TYPE:\n        logEvent(map);\n        break;\n      case CUSTOM_ATTRIBUTE_ACTION_TYPE:\n        setCustomAttribute(map);\n        break;\n      case CHANGE_USER_ACTION_TYPE:\n        changeUser(map);\n        break;\n      default:\n        BrazeLogger.w(TAG, \"Got unknown action type: \" + actionType);\n        break;\n    }\n  }\n\n  private void logEvent(Map<String, Object> tagParameterMap) {\n    String eventName = String.valueOf(tagParameterMap.remove(EVENT_NAME_VARIABLE));\n    Braze.getInstance(sApplicationContext).logCustomEvent(eventName, parseMapIntoProperties(tagParameterMap));\n  }\n\n  private BrazeProperties parseMapIntoProperties(Map<String, Object> map) {\n    BrazeProperties brazeProperties = new BrazeProperties();\n    for (Map.Entry<String, Object> entry : map.entrySet()) {\n      final Object value = entry.getValue();\n      final String key = entry.getKey();\n      if (value instanceof Boolean) {\n        brazeProperties.addProperty(key, (Boolean) value);\n      } else if (value instanceof Integer) {\n        brazeProperties.addProperty(key, (Integer) value);\n      } else if (value instanceof Date) {\n        brazeProperties.addProperty(key, (Date) value);\n      } else if (value instanceof Long) {\n        brazeProperties.addProperty(key, (Long) value);\n      } else if (value instanceof String) {\n        brazeProperties.addProperty(key, (String) value);\n      } else if (value instanceof Double) {\n        brazeProperties.addProperty(key, (Double) value);\n      } else {\n        BrazeLogger.w(TAG, \"Failed to parse value into a BrazeProperties \"\n            + \"accepted type. Key: '\" + key + \"' Value: '\" + value + \"'\");\n      }\n    }\n\n    return brazeProperties;\n  }\n\n  private void setCustomAttribute(Map<String, Object> tagParameterMap) {\n    BrazeUser brazeUser = Braze.getInstance(sApplicationContext).getCurrentUser();\n    if (brazeUser == null) {\n      BrazeLogger.w(TAG, \"BrazeUser was null. Returning.\");\n      return;\n    }\n    String key = String.valueOf(tagParameterMap.get(CUSTOM_ATTRIBUTE_KEY));\n    Object value = tagParameterMap.get(CUSTOM_ATTRIBUTE_VALUE_KEY);\n\n    if (value instanceof Boolean) {\n      brazeUser.setCustomUserAttribute(key, (Boolean) value);\n    } else if (value instanceof Integer) {\n      brazeUser.setCustomUserAttribute(key, (Integer) value);\n    } else if (value instanceof Long) {\n      brazeUser.setCustomUserAttribute(key, (Long) value);\n    } else if (value instanceof String) {\n      brazeUser.setCustomUserAttribute(key, (String) value);\n    } else if (value instanceof Double) {\n      brazeUser.setCustomUserAttribute(key, (Double) value);\n    } else if (value instanceof Float) {\n      brazeUser.setCustomUserAttribute(key, (Float) value);\n    } else {\n      BrazeLogger.w(TAG, \"Failed to parse value into a custom \"\n          + \"attribute accepted type. Key: '\" + key + \"' Value: '\" + value + \"'\");\n    }\n  }\n\n  private void changeUser(Map<String, Object> tagParameterMap) {\n    String userId = String.valueOf(tagParameterMap.get(CHANGE_USER_ID_VARIABLE));\n    Braze.getInstance(sApplicationContext).changeUser(userId);\n  }\n}\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/java/com/braze/googletagmanager/GtmApplication.java",
    "content": "package com.braze.googletagmanager;\n\nimport android.app.Application;\nimport android.util.Log;\n\nimport com.braze.Braze;\nimport com.braze.BrazeActivityLifecycleCallbackListener;\nimport com.braze.configuration.BrazeConfig;\nimport com.braze.support.BrazeLogger;\n\npublic class GtmApplication extends Application {\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    BrazeLogger.setLogLevel(Log.VERBOSE);\n    BrazeGtmTagProvider.setApplicationContext(this.getApplicationContext());\n    Braze.configure(this.getApplicationContext(), new BrazeConfig.Builder()\n        .setApiKey(\"4149fcbf-ee7a-45a8-8e89-e17e9fec1306\")\n        .setFirebaseCloudMessagingSenderIdKey(\"901477453852\")\n        .setIsFirebaseCloudMessagingRegistrationEnabled(true)\n        .setHandlePushDeepLinksAutomatically(true)\n        .build()\n    );\n    registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener());\n  }\n}\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/java/com/braze/googletagmanager/MainActivity.java",
    "content": "package com.braze.googletagmanager;\n\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.google.firebase.analytics.FirebaseAnalytics;\n\npublic class MainActivity extends AppCompatActivity {\n  private FirebaseAnalytics mFirebaseAnalytics;\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n    mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);\n\n    final EditText userIdInput = findViewById(R.id.editTextUserId);\n    Button submitUserId = findViewById(R.id.buttonChangeUser);\n\n    submitUserId.setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        String userId = userIdInput.getText().toString();\n        if (userId == null || userId.length() == 0) {\n          showMessage(\"User Id should not be null or empty. Doing nothing.\");\n          return;\n        } else {\n          Bundle params = new Bundle();\n          params.putString(\"externalUserId\", userId);\n          mFirebaseAnalytics.logEvent(\"changeUser\", params);\n        }\n      }\n    });\n\n    findViewById(R.id.bLogEvent).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        Bundle params = new Bundle();\n        params.putString(\"eventName\", \"played_song_event\");\n        params.putInt(\"intParam\", 1);\n        params.putBoolean(\"boolParam\", true);\n        params.putString(\"stringParam\", \"vantage\");\n        mFirebaseAnalytics.logEvent(\"logEvent\", params);\n      }\n    });\n\n    findViewById(R.id.bLogUserProperty).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        Bundle params = new Bundle();\n        params.putString(\"customAttributeKey\", \"favorite song\");\n        params.putString(\"customAttributeValue\", \"Private Eyes\");\n        mFirebaseAnalytics.logEvent(\"customAttribute\", params);\n      }\n    });\n  }\n\n  private void showMessage(String message) {\n    Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();\n  }\n}\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/activity_main\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n  android:paddingTop=\"@dimen/activity_vertical_margin\"\n  android:paddingRight=\"@dimen/activity_horizontal_margin\"\n  android:paddingBottom=\"@dimen/activity_vertical_margin\">\n  <EditText\n    android:id=\"@+id/editTextUserId\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_alignParentStart=\"true\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentTop=\"true\"\n    android:layout_alignParentEnd=\"true\"\n    android:layout_alignParentRight=\"true\"\n    android:ems=\"10\"\n    android:hint=\"@string/set_user_id_edit_text_hint\" />\n  <Button\n    android:id=\"@+id/buttonChangeUser\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@+id/editTextUserId\"\n    android:layout_alignParentStart=\"true\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentEnd=\"true\"\n    android:layout_alignParentRight=\"true\"\n    android:text=\"@string/set_user_id_button_prompt\" />\n  <Button\n    android:id=\"@+id/bLogEvent\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@+id/buttonChangeUser\"\n    android:layout_marginTop=\"5dp\"\n    android:text=\"@string/log_event\" />\n  <Button\n    android:id=\"@+id/bLogUserProperty\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@+id/bLogEvent\"\n    android:layout_marginTop=\"5dp\"\n    android:text=\"@string/log_user_property\" />\n</RelativeLayout>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n</resources>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#3F51B5</color>\n  <color name=\"colorPrimaryDark\">#303F9F</color>\n  <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/values/dimens.xml",
    "content": "<resources>\n  <!-- Default screen margins, per the Android Design guidelines. -->\n  <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n  <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string translatable=\"false\" name=\"app_name\">Google Tag Manager Sample</string>\n  <string translatable=\"false\" name=\"set_user_id_button_prompt\">Set User Id and request data flush</string>\n  <string translatable=\"false\" name=\"set_user_id_edit_text_hint\">Set a user id here</string>\n  <string name=\"log_event\">Log Event</string>\n  <string name=\"log_user_property\">Log User Property</string>\n</resources>\n"
  },
  {
    "path": "samples/google-tag-manager/src/main/res/values/styles.xml",
    "content": "<resources>\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "samples/hello-braze/README.md",
    "content": "This a sample Android application demonstrating the one-line Braze integration available\nfor API levels of 14 and above. Session tracking and InAppMessageManager registration are handled automatically via the \nAppboyLifecycleCallbackListener, which is registered in the app's ```Application.onCreate()``` method.\n"
  },
  {
    "path": "samples/hello-braze/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionName \"1.0\"\n  }\n\n  buildTypes {\n    debug {\n    }\n    release {\n      minifyEnabled true\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation project(':android-sdk-ui')\n}\n"
  },
  {
    "path": "samples/hello-braze/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.appboy.helloworld\">\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n  <application\n    android:icon=\"@drawable/ic_launcher_hello_braze\"\n    android:allowBackup=\"false\"\n    android:name=\"com.braze.helloworld.CustomApplication\"\n    android:label=\"@string/app_name\">\n    <activity\n      android:name=\"com.braze.helloworld.MainActivity\"\n      android:label=\"@string/app_name\"\n      android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n  </application>\n</manifest>\n"
  },
  {
    "path": "samples/hello-braze/src/main/java/com/braze/helloworld/CustomApplication.java",
    "content": "package com.braze.helloworld;\n\nimport android.app.Application;\nimport android.content.res.Resources;\nimport android.util.Log;\n\nimport com.appboy.helloworld.R;\nimport com.braze.Braze;\nimport com.braze.BrazeActivityLifecycleCallbackListener;\nimport com.braze.configuration.BrazeConfig;\nimport com.braze.support.BrazeLogger;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CustomApplication extends Application {\n\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    BrazeLogger.setLogLevel(Log.VERBOSE);\n    configureAppboyAtRuntime();\n    registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener());\n  }\n\n  private void configureAppboyAtRuntime() {\n\n    Resources resources = getResources();\n    BrazeConfig brazeConfig = new BrazeConfig.Builder()\n        .setApiKey(\"dd162bff-b14e-4d87-9bf0-fec609a77ca4\")\n        .setIsFirebaseCloudMessagingRegistrationEnabled(false)\n        .setAdmMessagingRegistrationEnabled(false)\n        .setSessionTimeout(11)\n        .setHandlePushDeepLinksAutomatically(true)\n        .setSmallNotificationIcon(resources.getResourceEntryName(R.drawable.ic_launcher_hello_braze))\n        .setLargeNotificationIcon(resources.getResourceEntryName(R.drawable.ic_launcher_hello_braze))\n        .setTriggerActionMinimumTimeIntervalSeconds(5)\n        .setIsLocationCollectionEnabled(false)\n        .setNewsfeedVisualIndicatorOn(true)\n        .setDefaultNotificationAccentColor(0xFFf33e3e)\n        .setBadNetworkDataFlushInterval(120)\n        .setGoodNetworkDataFlushInterval(60)\n        .setGreatNetworkDataFlushInterval(10)\n        .build();\n    Braze.configure(this, brazeConfig);\n  }\n}\n"
  },
  {
    "path": "samples/hello-braze/src/main/java/com/braze/helloworld/MainActivity.java",
    "content": "package com.braze.helloworld;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.Toast;\n\nimport com.appboy.helloworld.R;\nimport com.braze.Braze;\nimport com.braze.support.StringUtils;\n\npublic class MainActivity extends Activity {\n  private EditText mNickname;\n  private EditText mHighScore;\n  private EditText mUserId;\n  private Context mApplicationContext;\n\n  // These events will be shown in the Braze dashboard.\n  private static final String CUSTOM_CLICK_EVENT = \"clicked submit\";\n  private static final String HIGH_SCORE_ATTRIBUTE_KEY = \"user high score\";\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.main_activity);\n\n    // It is good practice to always get an instance of the Braze singleton using the application\n    // context.\n    mApplicationContext = this.getApplicationContext();\n\n    mNickname = findViewById(R.id.hello_high_score_nickname);\n    mHighScore = findViewById(R.id.hello_high_score);\n    mUserId = findViewById(R.id.hello_user_id);\n    Button submit = findViewById(R.id.hello_submit);\n\n    submit.setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        // Validate the nickname and high score, then send off to the server.\n        String nickname = mNickname.getEditableText().toString();\n        String highScore = mHighScore.getEditableText().toString();\n        String userId = mUserId.getEditableText().toString();\n\n        if (!StringUtils.isNullOrBlank(nickname) && !StringUtils.isNullOrBlank(highScore)) {\n          // Assign the current user an userId. You can search for this user using this external user id on the\n          // dashboard\n          Braze.getInstance(mApplicationContext).changeUser(userId);\n\n          // Send the custom event for the click\n          Braze.getInstance(mApplicationContext).logCustomEvent(CUSTOM_CLICK_EVENT);\n\n          // Log the custom attribute of \"nickname : highScore\"\n          String attributeString = String.format(\"%s : %s\", nickname, highScore);\n          Braze.getInstance(mApplicationContext).getCurrentUser()\n              .setCustomUserAttribute(HIGH_SCORE_ATTRIBUTE_KEY, attributeString);\n          displayToast(\"Sent off button click event and updated high score attribute for user \" + userId);\n        } else {\n          displayToast(\"All fields must be filled to submit.\");\n        }\n      }\n    });\n  }\n\n  // Displays a long toast to the user.\n  private void displayToast(String message) {\n    Toast.makeText(this, message, Toast.LENGTH_LONG).show();\n  }\n}\n"
  },
  {
    "path": "samples/hello-braze/src/main/res/layout/main_activity.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:paddingLeft=\"16dp\"\n  android:paddingRight=\"16dp\"\n  android:paddingTop=\"16dp\"\n  android:paddingBottom=\"16dp\">\n\n    <TextView\n        android:text=\"@string/instructions_body\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:id=\"@+id/hello_instructions\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_centerHorizontal=\"true\"\n        android:textColor=\"#f33e3e\"/>\n  <Button\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\"\n    android:text=\"@string/high_score_submit_hint\"\n    android:id=\"@+id/hello_submit\"\n    android:layout_alignParentBottom=\"true\"/>\n  <EditText\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:inputType=\"textPersonName\"\n    android:hint=\"@string/high_score_nickname_hint\"\n    android:ems=\"10\"\n    android:id=\"@+id/hello_high_score_nickname\"\n    android:layout_below=\"@+id/hello_instructions\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentStart=\"true\"\n    android:layout_marginTop=\"77dp\"/>\n  <EditText\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:inputType=\"number\"\n    android:ems=\"10\"\n    android:id=\"@+id/hello_high_score\"\n    android:hint=\"@string/high_score_hint\"\n    android:layout_below=\"@+id/hello_high_score_nickname\"\n    android:layout_alignParentLeft=\"true\"\n    android:layout_alignParentStart=\"true\"/>\n  <EditText\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:ems=\"10\"\n    android:id=\"@+id/hello_user_id\"\n    android:layout_alignBottom=\"@+id/hello_high_score_nickname\"\n    android:layout_alignParentLeft=\"true\"\n    android:hint=\"@string/user_id_hint\"\n    android:layout_alignParentStart=\"true\"\n    android:layout_marginBottom=\"31dp\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "samples/hello-braze/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!--Replace this stand-in key with your actual Braze API key from the dashboard-->\n  <string translatable=\"false\" name=\"com_braze_api_key\">YOUR-BRAZE-API-KEY-HERE</string>\n\n</resources>\n"
  },
  {
    "path": "samples/hello-braze/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string translatable=\"false\" name=\"app_name\">Hello Braze</string>\n\n  <string translatable=\"false\" name=\"high_score_nickname_hint\">nickname</string>\n  <string translatable=\"false\" name=\"high_score_hint\">score</string>\n  <string translatable=\"false\" name=\"high_score_submit_hint\">Submit</string>\n  <string translatable=\"false\" name=\"instructions_body\">Submit a nickname with your high score.</string>\n  <string translatable=\"false\" name=\"user_id_hint\">Set a User Id</string>\n</resources>\n"
  },
  {
    "path": "samples/hms-push-sample/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.hms_sample\"\n    // HMS requires a min SDK version of 17, which is\n    // (as of 3/9/2020) higher than our min of 16\n    minSdkVersion 17 //rootProject.ext.minSdkVersion\n    // Huawei has not provided an SDK ready for\n    // API 31 so this target version is on 30\n    targetSdkVersion 30\n    versionCode 1\n    versionName \"1.0\"\n    testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n  }\n  signingConfigs {\n    release {\n      storeFile file(\"hms_sample_keystore.keystore\")\n      storePassword \"Z3SXw5ZyU!Up\"\n      keyAlias \"easter_egg\"\n      keyPassword \"Z3SXw5ZyU!Up\"\n    }\n  }\n  buildTypes {\n    debug {\n      minifyEnabled false\n      signingConfig signingConfigs.release\n      debuggable true\n    }\n    release {\n      minifyEnabled false\n      signingConfig signingConfigs.release\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n\n  kotlinOptions {\n    jvmTarget = \"1.8\"\n  }\n}\n\ndependencies {\n  implementation(project(':android-sdk-ui')) {\n    exclude group: 'com.google.firebase'\n    exclude group: 'com.google.android.gms'\n  }\n  implementation \"org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}\"\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n  implementation \"androidx.constraintlayout:constraintlayout:${ANDROIDX_CONSTRAINT_LAYOUT_VERSION}\"\n  implementation \"androidx.coordinatorlayout:coordinatorlayout:${ANDROIDX_COORDINATOR_LAYOUT_VERSION}\"\n  implementation \"com.huawei.hms:push:${HUAWEI_HMS_PUSH_VERSION}\"\n}\n\nrepositories {\n  // Note that for security reasons, this repository should\n  // not be added at a higher level than this sample app's\n  // gradle file.\n  maven {\n    url \"https://developer.huawei.com/repo/\"\n  }\n}\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.braze.hms_sample\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n  <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n  <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n  <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n  <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n\n  <application\n    android:name=\".HmsApplication\"\n    android:allowBackup=\"false\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:roundIcon=\"@mipmap/ic_launcher_round\"\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\">\n    <activity\n      android:name=\"com.braze.hms_sample.MainActivity\"\n      android:label=\"@string/app_name\"\n      android:theme=\"@style/AppTheme.NoActionBar\"\n      android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n    </activity>\n    <service\n      android:name=\"com.braze.hms_sample.CustomPushService\"\n      android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"com.huawei.push.action.MESSAGING_EVENT\" />\n      </intent-filter>\n    </service>\n    <meta-data\n      android:name=\"com.huawei.hms.client.appid\"\n      android:value=\"appid=102077259\" />\n  </application>\n</manifest>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/java/com/braze/hms_sample/CustomPushService.kt",
    "content": "package com.braze.hms_sample\n\nimport android.util.Log\nimport com.braze.Braze\nimport com.braze.push.BrazeHuaweiPushHandler\nimport com.huawei.hms.aaid.HmsInstanceId\nimport com.huawei.hms.push.HmsMessageService\nimport com.huawei.hms.push.RemoteMessage\n\nclass CustomPushService : HmsMessageService() {\n\n    companion object {\n        val TAG: String = toString()\n    }\n\n    override fun onNewToken(token: String?) {\n        super.onNewToken(token)\n\n        @Suppress(\"DEPRECATION\")\n        val appId = com.huawei.agconnect.config\n            .AGConnectServicesConfig\n            .fromContext(applicationContext)\n            .getString(\"client/app_id\")\n        val pushToken = HmsInstanceId.getInstance(applicationContext).getToken(appId, \"HCM\")\n        Log.i(TAG, \"Got Huawei push token $pushToken\")\n        Braze.getInstance(applicationContext).registeredPushToken = token\n    }\n\n    override fun onMessageReceived(hmsRemoteMessage: RemoteMessage?) {\n        super.onMessageReceived(hmsRemoteMessage)\n\n        if (BrazeHuaweiPushHandler.handleHmsRemoteMessageData(applicationContext, hmsRemoteMessage?.dataOfMap)) {\n            Log.i(TAG, \"Braze has handled Huawei push notification.\")\n        }\n    }\n}\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/java/com/braze/hms_sample/HmsApplication.kt",
    "content": "package com.braze.hms_sample\n\nimport android.app.Application\nimport android.content.Context\nimport android.util.Log\nimport com.braze.Braze\nimport com.braze.BrazeActivityLifecycleCallbackListener\nimport com.braze.configuration.BrazeConfig\nimport com.braze.support.BrazeLogger\n\nclass HmsApplication : Application() {\n  override fun onCreate() {\n    super.onCreate()\n    BrazeLogger.logLevel = Log.VERBOSE\n\n    // Clear it out\n    Braze.configure(this.applicationContext, null)\n\n    // Get the custom env info, if it's there\n    val prefs = this.applicationContext.getSharedPreferences(CUSTOM_ENV_PREFS_NAME, Context.MODE_PRIVATE)\n    if (prefs.contains(CUSTOM_ENV_ENDPOINT_KEY) && prefs.contains(CUSTOM_ENV_API_KEY_KEY)) {\n      val apiKey = prefs.getString(CUSTOM_ENV_API_KEY_KEY, \"\")\n      val endpoint = prefs.getString(CUSTOM_ENV_ENDPOINT_KEY, \"\")\n\n      Braze.configure(this.applicationContext, BrazeConfig.Builder()\n          .setApiKey(apiKey!!)\n          .setCustomEndpoint(endpoint!!)\n          .setHandlePushDeepLinksAutomatically(true)\n          .build()\n      )\n    } else {\n      Braze.configure(this.applicationContext, BrazeConfig.Builder()\n          .setApiKey(\"71125d96-05fb-4df2-80ff-fca220d9d33b\")\n          .setHandlePushDeepLinksAutomatically(true)\n          .build()\n      )\n    }\n\n    registerActivityLifecycleCallbacks(BrazeActivityLifecycleCallbackListener())\n  }\n\n  companion object {\n    const val CUSTOM_ENV_PREFS_NAME = \"custom_env_prefs\"\n    const val CUSTOM_ENV_ENDPOINT_KEY = \"custom_env_endpoint\"\n    const val CUSTOM_ENV_API_KEY_KEY = \"custom_env_api_key\"\n  }\n}\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/java/com/braze/hms_sample/MainActivity.kt",
    "content": "package com.braze.hms_sample\n\nimport android.annotation.SuppressLint\nimport android.app.AlarmManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.util.Log\nimport android.widget.Button\nimport android.widget.EditText\nimport androidx.appcompat.app.AppCompatActivity\nimport com.braze.Braze\nimport com.braze.support.BrazeLogger\nimport com.braze.support.IntentUtils\nimport com.huawei.agconnect.AGConnectOptionsBuilder\nimport com.huawei.hms.aaid.HmsInstanceId\nimport kotlin.concurrent.thread\n\nclass MainActivity : AppCompatActivity() {\n\n  companion object {\n    private val TAG: String = this::class.java.name\n  }\n\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setContentView(R.layout.activity_main)\n\n    val userIdInput = findViewById<EditText>(R.id.etUserId)\n      Braze.getInstance(this).getCurrentUser {\n          userIdInput.post {\n              userIdInput.setText(it.userId)\n          }\n      }\n    findViewById<Button>(R.id.bSubmitUserId).setOnClickListener {\n      // Get the user id\n      val userId: String = userIdInput.text.toString()\n      Braze.getInstance(this).changeUser(userId)\n    }\n\n    findViewById<Button>(R.id.bGetAndSendToken).setOnClickListener {\n      getToken(this)\n    }\n\n    findViewById<Button>(R.id.bResetCustomEnvironment).setOnClickListener {\n      deleteEnvPrefs()\n      restartApp(this)\n    }\n\n    findViewById<Button>(R.id.bSetCustomEnvironment).setOnClickListener {\n      val customApiKey: String = findViewById<EditText>(R.id.etSetCustomApiKey).text.toString()\n      val customEndpoint: String = findViewById<EditText>(R.id.etSetCustomEndpoint).text.toString()\n      setCustomEnv(customApiKey, customEndpoint)\n      restartApp(this)\n    }\n  }\n\n  @SuppressLint(\"ApplySharedPref\")\n  private fun setCustomEnv(apiKey: String, endpoint: String) {\n    // Commit since we're restarting immediately afterwards\n    this.getSharedPreferences(HmsApplication.CUSTOM_ENV_PREFS_NAME, Context.MODE_PRIVATE)\n        .edit()\n        .putString(HmsApplication.CUSTOM_ENV_ENDPOINT_KEY, endpoint)\n        .putString(HmsApplication.CUSTOM_ENV_API_KEY_KEY, apiKey)\n        .commit()\n  }\n\n  @SuppressLint(\"ApplySharedPref\")\n  private fun deleteEnvPrefs() {\n    // Commit since we're restarting immediately afterwards\n    this.getSharedPreferences(HmsApplication.CUSTOM_ENV_PREFS_NAME, Context.MODE_PRIVATE)\n        .edit()\n        .clear()\n        .commit()\n  }\n\n  /**\n   * Obtain a token.\n   */\n  private fun getToken(context: Context) {\n    thread(start = true) {\n      try {\n        val appId = AGConnectOptionsBuilder().build(context).getString(\"client/app_id\")\n        val pushToken = HmsInstanceId.getInstance(context).getToken(appId, \"HCM\")\n        Braze.getInstance(context).registeredPushToken = pushToken!!\n        Log.i(TAG, \"Got HMS push token $pushToken\")\n      } catch (e: Exception) {\n        Log.e(TAG, \"getToken failed, $e\", e)\n      }\n    }\n  }\n\n  private fun restartApp(context: Context) {\n    val startActivity = Intent(this, MainActivity::class.java)\n    val pendingIntentId = 109829837\n    val pendingIntent = PendingIntent.getActivity(context, pendingIntentId, startActivity, PendingIntent.FLAG_CANCEL_CURRENT or IntentUtils.getImmutablePendingIntentFlags())\n    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager\n    alarmManager[AlarmManager.RTC, System.currentTimeMillis() + 1000] = pendingIntent\n    BrazeLogger.i(TAG, \"Restarting application to apply new environment values\")\n    System.exit(0)\n  }\n}\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:width=\"108dp\"\n  android:height=\"108dp\"\n  android:viewportWidth=\"108\"\n  android:viewportHeight=\"108\">\n  <path\n    android:fillColor=\"#008577\"\n    android:pathData=\"M0,0h108v108h-108z\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M9,0L9,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,0L19,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M29,0L29,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M39,0L39,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M49,0L49,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M59,0L59,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M69,0L69,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M79,0L79,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M89,0L89,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M99,0L99,108\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,9L108,9\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,19L108,19\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,29L108,29\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,39L108,39\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,49L108,49\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,59L108,59\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,69L108,69\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,79L108,79\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,89L108,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,99L108,99\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,29L89,29\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,39L89,39\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,49L89,49\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,59L89,59\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,69L89,69\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,79L89,79\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M29,19L29,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M39,19L39,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M49,19L49,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M59,19L59,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M69,19L69,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n  <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M79,19L79,89\"\n    android:strokeWidth=\"0.8\"\n    android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:aapt=\"http://schemas.android.com/aapt\"\n  android:width=\"108dp\"\n  android:height=\"108dp\"\n  android:viewportWidth=\"108\"\n  android:viewportHeight=\"108\">\n  <path\n    android:fillType=\"evenOdd\"\n    android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n    android:strokeWidth=\"1\"\n    android:strokeColor=\"#00000000\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient\n        android:endX=\"78.5885\"\n        android:endY=\"90.9159\"\n        android:startX=\"48.7653\"\n        android:startY=\"61.0927\"\n        android:type=\"linear\">\n        <item\n          android:color=\"#44000000\"\n          android:offset=\"0.0\" />\n        <item\n          android:color=\"#00000000\"\n          android:offset=\"1.0\" />\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n    android:fillColor=\"#FFFFFF\"\n    android:fillType=\"nonZero\"\n    android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n    android:strokeWidth=\"1\"\n    android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\">\n  <include layout=\"@layout/content_main\" />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/layout/content_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:context=\".MainActivity\"\n  tools:showIn=\"@layout/activity_main\">\n  <EditText\n    android:id=\"@+id/etUserId\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"108dp\"\n    android:ems=\"10\"\n    android:hint=\"User ID\"\n    android:inputType=\"textPersonName\"\n    app:layout_constraintEnd_toEndOf=\"parent\"\n    app:layout_constraintHorizontal_bias=\"0.207\"\n    app:layout_constraintStart_toStartOf=\"parent\"\n    app:layout_constraintTop_toTopOf=\"parent\" />\n  <Button\n    android:id=\"@+id/bSubmitUserId\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"8dp\"\n    android:text=\"Submit\"\n    app:layout_constraintStart_toStartOf=\"@+id/etUserId\"\n    app:layout_constraintTop_toBottomOf=\"@+id/etUserId\" />\n  <Button\n    android:id=\"@+id/bGetAndSendToken\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"16dp\"\n    android:text=\"Manually Get Token\"\n    app:layout_constraintStart_toStartOf=\"@+id/bSubmitUserId\"\n    app:layout_constraintTop_toBottomOf=\"@+id/bSubmitUserId\" />\n  <EditText\n    android:id=\"@+id/etSetCustomEndpoint\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"32dp\"\n    android:ems=\"10\"\n    android:hint=\"Endpoint\"\n    android:inputType=\"textPersonName\"\n    app:layout_constraintStart_toStartOf=\"@+id/bGetAndSendToken\"\n    app:layout_constraintTop_toBottomOf=\"@+id/bGetAndSendToken\" />\n  <EditText\n    android:id=\"@+id/etSetCustomApiKey\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"8dp\"\n    android:ems=\"10\"\n    android:hint=\"API Key\"\n    android:inputType=\"textPersonName\"\n    app:layout_constraintStart_toStartOf=\"@+id/bGetAndSendToken\"\n    app:layout_constraintTop_toBottomOf=\"@+id/etSetCustomEndpoint\" />\n  <Button\n    android:id=\"@+id/bSetCustomEnvironment\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"8dp\"\n    android:text=\"Set Custom\"\n    app:layout_constraintStart_toStartOf=\"@+id/etSetCustomApiKey\"\n    app:layout_constraintTop_toBottomOf=\"@+id/etSetCustomApiKey\" />\n  <Button\n    android:id=\"@+id/bResetCustomEnvironment\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginStart=\"8dp\"\n    android:layout_marginTop=\"8dp\"\n    android:text=\"Reset To Default\"\n    app:layout_constraintStart_toEndOf=\"@+id/bSetCustomEnvironment\"\n    app:layout_constraintTop_toBottomOf=\"@+id/etSetCustomApiKey\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n</resources>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#008577</color>\n  <color name=\"colorPrimaryDark\">#00574B</color>\n  <color name=\"colorAccent\">#D81B60</color>\n</resources>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/values/dimens.xml",
    "content": "<resources>\n  <dimen name=\"fab_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string name=\"app_name\">HMS Push Sample</string>\n  <string name=\"action_settings\">Settings</string>\n</resources>\n"
  },
  {
    "path": "samples/hms-push-sample/src/main/res/values/styles.xml",
    "content": "<resources>\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n    <!-- Customize your theme here. -->\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n\n  <style name=\"AppTheme.NoActionBar\">\n    <item name=\"windowActionBar\">false</item>\n    <item name=\"windowNoTitle\">true</item>\n  </style>\n\n  <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\" />\n\n  <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\" />\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/README.md",
    "content": "# Manual Session Integration Sample\n\nThis sample app showcases manual session integration.\n\nNote that in most cases, we recommend using automatic session integration, which is the default in our [docs on session integration][1].\n\n[1]: https://www.braze.com/docs/developer_guide/platform_integration_guides/android/initial_sdk_setup/android_sdk_integration/#step-4-tracking-user-sessions-in-android\n"
  },
  {
    "path": "samples/manual-session-integration/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  compileSdkVersion rootProject.ext.compileSdkVersion\n  buildToolsVersion rootProject.ext.buildToolsVersion\n\n  defaultConfig {\n    applicationId \"com.braze.manualsessionintegration\"\n    minSdkVersion rootProject.ext.minSdkVersion\n    targetSdkVersion rootProject.ext.targetSdkVersion\n    versionCode 1\n    versionName \"1.0\"\n\n    testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n\n  }\n  buildTypes {\n    release {\n      minifyEnabled true\n      signingConfig signingConfigs.debug\n      debuggable true\n    }\n  }\n\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_1_8\n    targetCompatibility JavaVersion.VERSION_1_8\n  }\n}\n\ndependencies {\n  implementation project(':android-sdk-ui')\n  implementation \"androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}\"\n}\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.braze.manualsessionintegration\"\n          xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity android:name=\".MainActivity\"\n          android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/java/com/braze/manualsessionintegration/MainActivity.java",
    "content": "package com.braze.manualsessionintegration;\n\nimport android.os.Bundle;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.braze.Braze;\nimport com.braze.ui.inappmessage.BrazeInAppMessageManager;\n\npublic class MainActivity extends AppCompatActivity {\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n  }\n\n  @Override\n  public void onStart() {\n    super.onStart();\n    // Opens (or reopens) an Braze session.\n    // Note: This must be called in the onStart lifecycle method of EVERY Activity. Failure to do so\n    // will result in incomplete and/or erroneous analytics.\n    Braze.getInstance(this).openSession(this);\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    // Registers the BrazeInAppMessageManager for the current Activity. This Activity will now listen for\n    // in-app messages from Braze.\n    BrazeInAppMessageManager.getInstance().registerInAppMessageManager(this);\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    // Unregisters the BrazeInAppMessageManager.\n    BrazeInAppMessageManager.getInstance().unregisterInAppMessageManager(this);\n  }\n\n  @Override\n  public void onStop() {\n    super.onStop();\n    // Closes the current Braze session.\n    // Note: This must be called in the onStop lifecycle method of EVERY Activity. Failure to do so\n    // will result in incomplete and/or erroneous analytics.\n    Braze.getInstance(this).closeSession(this);\n  }\n}\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout android:id=\"@+id/activity_main\"\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:paddingBottom=\"@dimen/activity_vertical_margin\"\n  android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n  android:paddingRight=\"@dimen/activity_horizontal_margin\"\n  android:paddingTop=\"@dimen/activity_vertical_margin\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Hello World!\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values/braze.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <!-- General configuration -->\n  <!-- The API key is not shown to the user and thus we don't require that it be translated. -->\n  <string translatable=\"false\" name=\"com_braze_api_key\">YOUR-BRAZE-API-KEY-HERE</string>\n\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string translatable=\"false\" name=\"app_name\">ManualSessionIntegration</string>\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "samples/manual-session-integration/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':android-sdk-location'\ninclude ':android-sdk-ui'\ninclude ':android-sdk-unity'\ninclude ':droidboy'\ninclude ':samples:hello-braze'\ninclude ':samples:manual-session-integration'\ninclude ':samples:firebase-push'\ninclude ':samples:custom-broadcast'\ninclude ':samples:glide-image-integration'\ninclude ':samples:google-tag-manager'\n"
  }
]