diff --git a/kits/braze/braze-39/README.md b/kits/braze/braze-39/README.md
new file mode 100644
index 000000000..505038e74
--- /dev/null
+++ b/kits/braze/braze-39/README.md
@@ -0,0 +1,72 @@
+# Braze (formerly Appboy) Kit Integration
+
+This repository contains the [Braze](https://www.braze.com/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk).
+
+## Example App
+
+This repository contains an [example app](https://github.com/mparticle-integrations/mparticle-android-integration-appboy/tree/master/example) showing how to implement mParticle, Braze, and Firebase Cloud Messaging. The key changes you need to make to your app are below, and please also reference mParticle and Braze's documentation:
+
+- [Instrumenting Push](https://docs.mparticle.com/developers/sdk/android/push-notifications)
+- [Braze Documentation](https://docs.mparticle.com/integrations/braze/event)
+
+## 1. Adding the integration
+
+[See a full build.gradle example here](https://github.com/mparticle-integrations/mparticle-android-integration-appboy/blob/master/example/build.gradle)
+
+1. The Braze Kit requires that you add Braze's Maven server to your buildscript:
+
+ ```groovy
+ repositories {
+ maven { url "https://appboy.github.io/appboy-android-sdk/sdk" }
+ //Braze's library depends on the Google Support Library
+ google()
+ ...
+ }
+ ```
+
+2. Add the kit dependency to your app's `build.gradle`:
+
+ ```groovy
+ dependencies {
+ implementation 'com.mparticle:android-appboy-kit:5+'
+ }
+ ```
+
+## 2. Registering for Push
+
+mParticle's SDK takes care of registering for push notifications and passing tokens or instance IDs to the Braze SDK. [Follow the mParticle push notification documentation](https://docs.mparticle.com/developers/sdk/android/push-notifications#register-for-push-notifications) to instrument the SDK for push registration. You can skip over [this section of Braze's documentation](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/push_notifications/integration/#registering-for-push).
+
+## 3. Displaying Push
+
+[See a full example of an AndroidManifest.xml here](https://github.com/mparticle-integrations/mparticle-android-integration-appboy/blob/master/example/src/main/AndroidManifest.xml).
+
+mParticle's SDK also takes care of capturing incoming push notifications and passing the resulting `Intent` to Braze's `BrazePushReceiver`. Follow the [mParticle push notification documentation](https://docs.mparticle.com/developers/sdk/android/push-notifications#display-push-notifications) to ensure you add the correct services and receivers to your app's AndroidManifest.xml.
+
+## 4. Reacting to Push and Deeplinking
+
+There are a wide variety of implementation options available in Braze to deeplink a user when they tap a notification. There are **two specific requirements** to ensure automatic deeplinking works as intended.
+
+- `BrazePushReceiver`
+
+ Whereas up until now you should have nothing Braze-specific in your `AndroidManifest.xml`, using Braze's automatic deeplinking does require you to add their `BrazePushReceiver`. Note that you do not need to specify any Intent filters (for example to receive push tokens, since mParticle takes care of that). You just need to add the following:
+
+ ```xml
+
+ ```
+
+- `braze.xml`
+
+ For automatic deep-linking, you need to add a boolean resource named `com_braze_handle_push_deep_links_automatically`. This can be in any resource file, or you can name it `braze.xml`:
+
+ ```xml
+
+
+ true
+
+ ```
+
+From here you should be able to successfully test push via Braze! Braze offers many client-side configurable options via xml resources and otherwise. Please see review the rest of [their documentation here](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/push_notifications/integration/#step-3-add-deep-links) for more information.
+
+## License
+
+[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0)
diff --git a/kits/braze/braze-39/build.gradle b/kits/braze/braze-39/build.gradle
new file mode 100644
index 000000000..ffeff9f1d
--- /dev/null
+++ b/kits/braze/braze-39/build.gradle
@@ -0,0 +1,72 @@
+buildscript {
+ ext.kotlin_version = '2.0.20'
+ if (!project.hasProperty('version') || project.version.equals('unspecified')) {
+ project.version = '+'
+ }
+
+ repositories {
+ google()
+ mavenLocal()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.1.4'
+ classpath 'com.mparticle:android-kit-plugin:' + project.version
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+plugins {
+ id "org.sonarqube" version "3.5.0.2730"
+ id "org.jlleitschuh.gradle.ktlint" version "13.0.0"
+}
+
+sonarqube {
+ properties {
+ property "sonar.projectKey", "mparticle-android-integration-appboy"
+ property "sonar.organization", "mparticle"
+ property "sonar.host.url", "https://sonarcloud.io"
+ }
+}
+
+apply plugin: 'org.jlleitschuh.gradle.ktlint'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.mparticle.kit'
+
+android {
+ namespace 'com.mparticle.kits.appboy'
+ buildFeatures {
+ buildConfig = true
+ }
+ defaultConfig {
+ minSdkVersion 21
+ }
+ lint {
+ // Workaround for lint internal crash
+ abortOnError false
+ // Ignore obsolete custom lint checks from older fragment library
+ disable 'ObsoleteLintCustomCheck'
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+ testOptions {
+ unitTests.all {
+ jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED']
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ compileOnly 'com.google.firebase:firebase-messaging:[10.2.1, )'
+ api 'com.braze:android-sdk-ui:39.0.0'
+ testImplementation files('libs/java-json.jar')
+}
diff --git a/kits/braze/braze-39/consumer-proguard.pro b/kits/braze/braze-39/consumer-proguard.pro
new file mode 100644
index 000000000..84babb199
--- /dev/null
+++ b/kits/braze/braze-39/consumer-proguard.pro
@@ -0,0 +1,8 @@
+# These are the proguard rules specified by the Appboy SDK's documentation
+
+-dontwarn com.amazon.device.messaging.**
+-dontwarn bo.app.**
+-dontwarn com.braze.ui.**
+-dontwarn com.google.android.gms.**
+-keep class bo.app.** { *; }
+-keep class com.braze.** { *; }
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/build.gradle b/kits/braze/braze-39/example/build.gradle
new file mode 100644
index 000000000..b52ca8d32
--- /dev/null
+++ b/kits/braze/braze-39/example/build.gradle
@@ -0,0 +1,66 @@
+/**
+ *
+ * Example app build.gradle for using mParticle + Braze + Firebase Cloud Messaging
+ * Please see the inline comments below.
+ *
+ */
+
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ applicationId "com.mparticle.com.mparticle.kits.braze.example"
+ minSdk 16
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ maven { url "https://appboy.github.io/appboy-android-sdk/sdk" } //REQUIRED: Braze isn't available in jCenter or Maven Central - so you need to add their Maven Server
+ google()
+}
+
+buildscript {
+ repositories {
+ //REQUIRED: com.google.gms:google-services requires both jCenter and Google's Maven :rollseyes:
+ mavenCentral()
+ google()
+ }
+ dependencies {
+ classpath 'com.google.gms:google-services:4.2.0' //REQUIRED for Firebase
+ }
+}
+dependencies {
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+ implementation 'com.android.support:support-v4:28.0.0'
+ implementation 'com.android.support:support-media-compat:28.0.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+
+ // REQUIRED: Add the Braze (formerly Appboy) kit here
+ // this will also pull in mParticle's Core SDK (com.mparticle:android-core) as a transitive dependency
+ implementation 'com.mparticle:android-appboy-kit:5.6.5'
+
+ // REQUIRED for Firebase
+ implementation 'com.google.firebase:firebase-messaging:17.3.4'
+
+ // Not strictly required but strongly recommended so that mParticle and Braze can query for the Android Advertising ID
+ implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
+
+}
+
+apply plugin: 'com.google.gms.google-services' //REQUIRED for Firebase
diff --git a/kits/braze/braze-39/example/src/main/AndroidManifest.xml b/kits/braze/braze-39/example/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d8192e240
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/src/main/java/com/mparticle/kits/braze/example/MainActivity.java b/kits/braze/braze-39/example/src/main/java/com/mparticle/kits/braze/example/MainActivity.java
new file mode 100644
index 000000000..e6254cd12
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/java/com/mparticle/kits/braze/example/MainActivity.java
@@ -0,0 +1,13 @@
+package com.mparticle.kits.braze.example;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+}
diff --git a/kits/braze/braze-39/example/src/main/res/drawable-v24/ic_launcher_foreground.xml b/kits/braze/braze-39/example/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..1f6bb2906
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kits/braze/braze-39/example/src/main/res/drawable/ic_launcher_background.xml b/kits/braze/braze-39/example/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..0d025f9bf
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kits/braze/braze-39/example/src/main/res/layout/activity_main.xml b/kits/braze/braze-39/example/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..73ddbe136
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher.png b/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..898f3ed59
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher_round.png b/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..dffca3601
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher.png b/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..64ba76f75
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher_round.png b/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..dae5e0823
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..e5ed46597
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..14ed0af35
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b0907cac3
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..d8ae03154
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..2c18de9e6
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..beed3cdd2
Binary files /dev/null and b/kits/braze/braze-39/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/kits/braze/braze-39/example/src/main/res/values/appboy.xml b/kits/braze/braze-39/example/src/main/res/values/appboy.xml
new file mode 100644
index 000000000..21ef222fa
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/values/appboy.xml
@@ -0,0 +1,4 @@
+
+
+ true
+
\ No newline at end of file
diff --git a/kits/braze/braze-39/example/src/main/res/values/colors.xml b/kits/braze/braze-39/example/src/main/res/values/colors.xml
new file mode 100644
index 000000000..69b22338c
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/kits/braze/braze-39/example/src/main/res/values/strings.xml b/kits/braze/braze-39/example/src/main/res/values/strings.xml
new file mode 100644
index 000000000..b6c2f04cd
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Braze Example
+
diff --git a/kits/braze/braze-39/example/src/main/res/values/styles.xml b/kits/braze/braze-39/example/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/kits/braze/braze-39/example/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/kits/braze/braze-39/gradle.properties b/kits/braze/braze-39/gradle.properties
new file mode 100644
index 000000000..56fb79f85
--- /dev/null
+++ b/kits/braze/braze-39/gradle.properties
@@ -0,0 +1,4 @@
+android.enableJetifier=true
+android.useAndroidX=true
+org.gradle.daemon=true
+
diff --git a/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.jar b/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..41d9927a4
Binary files /dev/null and b/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.properties b/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..e1bef7e87
--- /dev/null
+++ b/kits/braze/braze-39/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/kits/braze/braze-39/gradlew b/kits/braze/braze-39/gradlew
new file mode 100755
index 000000000..1b6c78733
--- /dev/null
+++ b/kits/braze/braze-39/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/kits/braze/braze-39/gradlew.bat b/kits/braze/braze-39/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/kits/braze/braze-39/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kits/braze/braze-39/libs/java-json.jar b/kits/braze/braze-39/libs/java-json.jar
new file mode 100755
index 000000000..2f211e366
Binary files /dev/null and b/kits/braze/braze-39/libs/java-json.jar differ
diff --git a/kits/braze/braze-39/settings.gradle.kts b/kits/braze/braze-39/settings.gradle.kts
new file mode 100644
index 000000000..f49fd9f1b
--- /dev/null
+++ b/kits/braze/braze-39/settings.gradle.kts
@@ -0,0 +1,2 @@
+rootProject.name = "android-appboy-kit"
+include(":")
diff --git a/kits/braze/braze-39/src/main/AndroidManifest.xml b/kits/braze/braze-39/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c4e6c98d7
--- /dev/null
+++ b/kits/braze/braze-39/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/kits/braze/braze-39/src/main/kotlin/com/mparticle/kits/AppboyKit.kt b/kits/braze/braze-39/src/main/kotlin/com/mparticle/kits/AppboyKit.kt
new file mode 100644
index 000000000..c44edcf3b
--- /dev/null
+++ b/kits/braze/braze-39/src/main/kotlin/com/mparticle/kits/AppboyKit.kt
@@ -0,0 +1,1396 @@
+package com.mparticle.kits
+
+import android.app.Application
+import android.app.Application.ActivityLifecycleCallbacks
+import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import com.braze.Braze
+import com.braze.BrazeActivityLifecycleCallbackListener
+import com.braze.BrazeUser
+import com.braze.configuration.BrazeConfig
+import com.braze.enums.BrazeSdkMetadata
+import com.braze.enums.Gender
+import com.braze.enums.Month
+import com.braze.enums.NotificationSubscriptionType
+import com.braze.enums.SdkFlavor
+import com.braze.events.IValueCallback
+import com.braze.models.outgoing.BrazeProperties
+import com.braze.push.BrazeFirebaseMessagingService
+import com.braze.push.BrazeNotificationUtils.isBrazePushMessage
+import com.google.firebase.messaging.RemoteMessage
+import com.mparticle.MPEvent
+import com.mparticle.MParticle
+import com.mparticle.MParticle.IdentityType
+import com.mparticle.MParticle.UserAttributes
+import com.mparticle.commerce.CommerceEvent
+import com.mparticle.commerce.Impression
+import com.mparticle.commerce.Product
+import com.mparticle.commerce.Promotion
+import com.mparticle.consent.ConsentState
+import com.mparticle.identity.MParticleUser
+import com.mparticle.internal.Logger
+import com.mparticle.kits.CommerceEventUtils.OnAttributeExtracted
+import com.mparticle.kits.KitIntegration.AttributeListener
+import com.mparticle.kits.KitIntegration.CommerceListener
+import com.mparticle.kits.KitIntegration.EventListener
+import com.mparticle.kits.KitIntegration.IdentityListener
+import com.mparticle.kits.KitIntegration.PushListener
+import com.mparticle.kits.KitIntegration.UserAttributeListener
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.math.BigDecimal
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.EnumSet
+import java.util.LinkedList
+import kotlin.collections.HashMap
+
+/**
+ * mParticle client-side Appboy integration
+ */
+open class AppboyKit :
+ KitIntegration(),
+ AttributeListener,
+ CommerceListener,
+ KitIntegration.EventListener,
+ PushListener,
+ IdentityListener,
+ KitIntegration.UserAttributeListener {
+ var enableTypeDetection = false
+ var bundleCommerceEvents = false
+ var isMpidIdentityType = false
+ var identityType: IdentityType? = null
+ var subscriptionGroupIds: MutableMap? = mutableMapOf()
+ private val dataFlushHandler = Handler()
+ private var dataFlushRunnable: Runnable? = null
+ private var forwardScreenViews = false
+ private lateinit var updatedInstanceId: String
+
+ override fun getName() = NAME
+
+ public override fun onKitCreate(
+ settings: Map,
+ context: Context,
+ ): List? {
+ val key = settings[APPBOY_KEY]
+ require(!KitUtils.isEmpty(key)) { "Braze key is empty." }
+
+ // try to get endpoint from the host setting
+ val authority = settings[HOST]
+ if (!KitUtils.isEmpty(authority)) {
+ setAuthority(authority)
+ }
+ val enableDetectionType = settings[ENABLE_TYPE_DETECTION]
+ if (!KitUtils.isEmpty(enableDetectionType)) {
+ try {
+ enableTypeDetection = enableDetectionType.toBoolean()
+ } catch (e: Exception) {
+ Logger.warning("Braze, unable to parse \"enableDetectionType\"")
+ }
+ }
+ val bundleCommerce = settings[BUNDLE_COMMERCE_EVENTS]
+ if (!KitUtils.isEmpty(bundleCommerce)) {
+ try {
+ bundleCommerceEvents = bundleCommerce.toBoolean()
+ } catch (e: Exception) {
+ bundleCommerceEvents = false
+ }
+ }
+ forwardScreenViews = settings[FORWARD_SCREEN_VIEWS].toBoolean()
+ subscriptionGroupIds =
+ settings[SUBSCRIPTION_GROUP_MAPPING]
+ ?.let {
+ getSubscriptionGroupIds(it)
+ }
+ if (key != null) {
+ val config =
+ BrazeConfig
+ .Builder()
+ .setApiKey(key)
+ .setSdkFlavor(SdkFlavor.MPARTICLE)
+ .setSdkMetadata(EnumSet.of(BrazeSdkMetadata.MPARTICLE))
+ .build()
+ Braze.configure(context, config)
+ }
+ dataFlushRunnable =
+ Runnable {
+ if (kitManager.isBackgrounded) {
+ Braze.getInstance(getContext()).requestImmediateDataFlush()
+ }
+ }
+ queueDataFlush()
+ if (setDefaultAppboyLifecycleCallbackListener) {
+ (context.applicationContext as Application).registerActivityLifecycleCallbacks(
+ BrazeActivityLifecycleCallbackListener() as ActivityLifecycleCallbacks,
+ )
+ }
+ setIdentityType(settings)
+
+ val user = MParticle.getInstance()?.Identity()?.currentUser
+ if (user != null) {
+ updateUser(user)
+ }
+ val userConsentState = currentUser?.consentState
+ userConsentState?.let {
+ setConsent(currentUser.consentState)
+ }
+ return null
+ }
+
+ fun setIdentityType(settings: Map) {
+ val userIdentificationType = settings[USER_IDENTIFICATION_TYPE]
+ if (!KitUtils.isEmpty(userIdentificationType)) {
+ if (userIdentificationType == "MPID") {
+ isMpidIdentityType = true
+ } else {
+ identityType = userIdentificationType?.let { IdentityType.valueOf(it) }
+ }
+ }
+ }
+
+ override fun setOptOut(optedOut: Boolean): List = emptyList()
+
+ override fun leaveBreadcrumb(breadcrumb: String): List = emptyList()
+
+ override fun logError(
+ message: String,
+ errorAttributes: Map,
+ ): List = emptyList()
+
+ override fun logException(
+ exception: Exception,
+ exceptionAttributes: Map,
+ message: String,
+ ): List = emptyList()
+
+ override fun logEvent(event: MPEvent): List {
+ val newAttributes: MutableMap = HashMap()
+ if (event.customAttributes == null) {
+ Braze.getInstance(context).logCustomEvent(event.eventName)
+ } else {
+ val properties = BrazeProperties()
+ val brazePropertiesSetter = BrazePropertiesSetter(properties, enableTypeDetection)
+ event.customAttributeStrings?.let { it ->
+ for ((key, value) in it) {
+ newAttributes[key] = brazePropertiesSetter.parseValue(key, value)
+ }
+ }
+ Braze.getInstance(context).logCustomEvent(event.eventName, properties)
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ val userAttributeSetter = UserAttributeSetter(value, enableTypeDetection)
+ event.customAttributeStrings?.let { it ->
+ for ((key, attributeValue) in it) {
+ val hashedKey =
+ KitUtils.hashForFiltering(event.eventType.value.toString() + event.eventName + key)
+
+ configuration.eventAttributesAddToUser?.get(hashedKey)?.let {
+ value.addToCustomAttributeArray(it, attributeValue)
+ }
+ configuration.eventAttributesRemoveFromUser?.get(hashedKey)?.let {
+ value.removeFromCustomAttributeArray(it, attributeValue)
+ }
+ configuration.eventAttributesSingleItemUser?.get(hashedKey)?.let {
+ userAttributeSetter.parseValue(it, attributeValue)
+ }
+ }
+ }
+ }
+
+ override fun onError() {
+ Logger.warning("unable to acquire user to add or remove custom user attributes from events")
+ }
+ },
+ )
+ }
+ queueDataFlush()
+ return listOf(ReportingMessage.fromEvent(this, event).setAttributes(newAttributes))
+ }
+
+ override fun logScreen(
+ screenName: String,
+ screenAttributes: Map?,
+ ): List =
+ if (forwardScreenViews) {
+ if (screenAttributes == null) {
+ Braze.getInstance(context).logCustomEvent(screenName)
+ } else {
+ val properties = BrazeProperties()
+ val propertyParser = BrazePropertiesSetter(properties, enableTypeDetection)
+ for ((key, value) in screenAttributes) {
+ propertyParser.parseValue(key, value)
+ }
+ Braze.getInstance(context).logCustomEvent(screenName, properties)
+ }
+ queueDataFlush()
+ val messages: MutableList = LinkedList()
+ messages.add(
+ ReportingMessage(
+ this,
+ ReportingMessage.MessageType.SCREEN_VIEW,
+ System.currentTimeMillis(),
+ screenAttributes,
+ ),
+ )
+ messages
+ } else {
+ emptyList()
+ }
+
+ override fun logLtvIncrease(
+ valueIncreased: BigDecimal,
+ valueTotal: BigDecimal,
+ eventName: String,
+ contextInfo: Map,
+ ): List = emptyList()
+
+ override fun logEvent(event: CommerceEvent): List {
+ val messages: MutableList = LinkedList()
+ if (!KitUtils.isEmpty(event.productAction) &&
+ event.productAction.equals(
+ Product.PURCHASE,
+ true,
+ ) &&
+ !event.products.isNullOrEmpty()
+ ) {
+ if (bundleCommerceEvents) {
+ logOrderLevelTransaction(event)
+ messages.add(ReportingMessage.fromEvent(this, event))
+ } else {
+ val productList = event.products
+ productList?.let {
+ for (product in productList) {
+ logTransaction(event, product)
+ }
+ }
+ }
+ messages.add(ReportingMessage.fromEvent(this, event))
+ } else {
+ if (bundleCommerceEvents) {
+ logOrderLevelTransaction(event)
+ messages.add(ReportingMessage.fromEvent(this, event))
+ } else {
+ val eventList = CommerceEventUtils.expand(event)
+ if (eventList != null) {
+ for (i in eventList.indices) {
+ try {
+ val e = eventList[i]
+ val map = mutableMapOf()
+ event.customAttributeStrings?.let { map.putAll(it) }
+ for (pair in map) {
+ e.customAttributes?.put(pair.key, pair.value)
+ }
+ logEvent(e)
+ messages.add(ReportingMessage.fromEvent(this, event))
+ } catch (e: Exception) {
+ Logger.warning("Failed to call logCustomEvent to Appboy kit: $e")
+ }
+ }
+ }
+ }
+ }
+ queueDataFlush()
+ return messages
+ }
+
+ override fun setUserAttribute(
+ keyIn: String,
+ attributeValue: String,
+ ) {
+ var key = keyIn
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ val userAttributeSetter = UserAttributeSetter(value, enableTypeDetection)
+
+ when (key) {
+ UserAttributes.CITY -> value.setHomeCity(attributeValue)
+ UserAttributes.COUNTRY -> value.setCountry(attributeValue)
+ UserAttributes.FIRSTNAME -> value.setFirstName(attributeValue)
+ UserAttributes.LASTNAME -> value.setLastName(attributeValue)
+ UserAttributes.MOBILE_NUMBER -> value.setPhoneNumber(attributeValue)
+ UserAttributes.ZIPCODE -> value.setCustomUserAttribute("Zip", attributeValue)
+ UserAttributes.AGE -> {
+ val calendar = getCalendarMinusYears(attributeValue)
+ if (calendar != null) {
+ value.setDateOfBirth(calendar[Calendar.YEAR], Month.JANUARY, 1)
+ } else {
+ Logger.warning("unable to set DateOfBirth for " + UserAttributes.AGE + " = " + value)
+ }
+ }
+ EMAIL_SUBSCRIBE -> {
+ when (attributeValue) {
+ OPTED_IN -> value.setEmailNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN)
+ UNSUBSCRIBED -> value.setEmailNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED)
+ SUBSCRIBED -> value.setEmailNotificationSubscriptionType(NotificationSubscriptionType.SUBSCRIBED)
+ else -> {
+ Logger.warning("unable to set email_subscribe with invalid value: " + value)
+ }
+ }
+ }
+ PUSH_SUBSCRIBE -> {
+ when (attributeValue) {
+ OPTED_IN -> value.setPushNotificationSubscriptionType(NotificationSubscriptionType.OPTED_IN)
+ UNSUBSCRIBED -> value.setPushNotificationSubscriptionType(NotificationSubscriptionType.UNSUBSCRIBED)
+ SUBSCRIBED -> value.setPushNotificationSubscriptionType(NotificationSubscriptionType.SUBSCRIBED)
+ else -> {
+ Logger.warning("unable to set push_subscribe with invalid value: " + value)
+ }
+ }
+ }
+ DOB -> useDobString(attributeValue, value)
+ UserAttributes.GENDER -> {
+ if (attributeValue.contains("fe")) {
+ value.setGender(Gender.FEMALE)
+ } else {
+ value.setGender(Gender.MALE)
+ }
+ }
+ else -> {
+ if (subscriptionGroupIds?.containsKey(key) == true) {
+ val groupId = subscriptionGroupIds?.get(key)
+ when (attributeValue.lowercase()) {
+ "true" -> {
+ groupId?.let { value.addToSubscriptionGroup(it) }
+ }
+
+ "false" -> {
+ groupId?.let { value.removeFromSubscriptionGroup(it) }
+ }
+
+ else -> {
+ Logger.warning(
+ "Unable to set Subscription Group ID for user attribute: $key due to invalid value data type. Expected Boolean.",
+ )
+ }
+ }
+ } else {
+ if (key.startsWith("$")) {
+ key = key.substring(1)
+ }
+ userAttributeSetter?.parseValue(key, attributeValue)
+ }
+ }
+ }
+ queueDataFlush()
+ }
+
+ override fun onError() {
+ Logger.warning("unable to set key: " + key + " with value: " + attributeValue)
+ }
+ },
+ )
+ }
+
+ // Expected Date Format @"yyyy'-'MM'-'dd"
+ private fun useDobString(
+ value: String,
+ user: BrazeUser,
+ ) {
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd")
+ try {
+ val calendar = Calendar.getInstance()
+ calendar.time = dateFormat.parse(value) as Date
+ val year = calendar[Calendar.YEAR]
+ val monthNum = calendar[Calendar.MONTH]
+ val month = Month.values()[monthNum] //
+ val day = calendar[Calendar.DAY_OF_MONTH]
+ user.setDateOfBirth(year, month, day)
+ } catch (e: Exception) {
+ Logger.warning("unable to set DateOfBirth for \"dob\" = " + value + ". Exception: " + e.message)
+ }
+ }
+
+ override fun setUserAttributeList(
+ key: String,
+ list: List,
+ ) {
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ val array = list.toTypedArray()
+ value.setCustomAttributeArray(key, array)
+ queueDataFlush()
+ }
+
+ override fun onError() {
+ Logger.warning("unable to set key: " + key + " with User Attribute List: " + list)
+ }
+ },
+ )
+ }
+
+ override fun onIncrementUserAttribute(
+ key: String?,
+ incrementedBy: Number?,
+ value: String?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun onRemoveUserAttribute(
+ key: String?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun onSetUserAttribute(
+ key: String?,
+ value: Any?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun onSetUserTag(
+ key: String?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun onSetUserAttributeList(
+ attributeKey: String?,
+ attributeValueList: MutableList?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun onSetAllUserAttributes(
+ userAttributes: MutableMap?,
+ userAttributeLists: MutableMap>?,
+ user: FilteredMParticleUser?,
+ ) {
+ }
+
+ override fun supportsAttributeLists(): Boolean = true
+
+ override fun onConsentStateUpdated(
+ oldState: ConsentState,
+ newState: ConsentState,
+ user: FilteredMParticleUser,
+ ) {
+ setConsent(newState)
+ }
+
+ private fun setConsent(consentState: ConsentState) {
+ val clientConsentSettings = parseToNestedMap(consentState.toString())
+
+ parseConsentMapping(settings[CONSENT_MAPPING_SDK]).iterator().forEach { currentConsent ->
+ val isConsentAvailable =
+ searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key)
+
+ if (isConsentAvailable != null) {
+ val isConsentGranted: Boolean =
+ JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean
+
+ when (currentConsent.value) {
+ "google_ad_user_data" ->
+ setConsentValueToBraze(
+ KEY_GOOGLE_AD_USER_DATA,
+ isConsentGranted,
+ )
+
+ "google_ad_personalization" ->
+ setConsentValueToBraze(
+ KEY_GOOGLE_AD_PERSONALIZATION,
+ isConsentGranted,
+ )
+ }
+ }
+ }
+ }
+
+ private fun setConsentValueToBraze(
+ key: String,
+ value: Boolean,
+ ) {
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(brazeUser: BrazeUser) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+
+ override fun onError() {
+ super.onError()
+ }
+ },
+ )
+ }
+
+ private fun parseConsentMapping(json: String?): Map {
+ if (json.isNullOrEmpty()) {
+ return emptyMap()
+ }
+ val jsonWithFormat = json.replace("\\", "")
+
+ return try {
+ JSONArray(jsonWithFormat)
+ .let { jsonArray ->
+ (0 until jsonArray.length())
+ .associate {
+ val jsonObject = jsonArray.getJSONObject(it)
+ val map = jsonObject.getString("map")
+ val value = jsonObject.getString("value")
+ map to value
+ }
+ }
+ } catch (jse: JSONException) {
+ Logger.warning(
+ jse,
+ "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.",
+ )
+ emptyMap()
+ }
+ }
+
+ private fun parseToNestedMap(jsonString: String): Map {
+ val topLevelMap = mutableMapOf()
+ try {
+ val jsonObject = JSONObject(jsonString)
+
+ for (key in jsonObject.keys()) {
+ val value = jsonObject.get(key)
+ if (value is JSONObject) {
+ topLevelMap[key] = parseToNestedMap(value.toString())
+ } else {
+ topLevelMap[key] = value
+ }
+ }
+ } catch (e: Exception) {
+ Logger.error(e, "The Braze kit was unable to parse the user's ConsentState, consent may not be set correctly on the Braze SDK")
+ }
+ return topLevelMap
+ }
+
+ private fun searchKeyInNestedMap(
+ map: Map<*, *>,
+ key: Any,
+ ): Any? {
+ if (map.isNullOrEmpty()) {
+ return null
+ }
+ try {
+ for ((mapKey, mapValue) in map) {
+ if (mapKey.toString().equals(key.toString(), ignoreCase = true)) {
+ return mapValue
+ }
+ if (mapValue is Map<*, *>) {
+ val foundValue = searchKeyInNestedMap(mapValue, key)
+ if (foundValue != null) {
+ return foundValue
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Logger.error(
+ e,
+ "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.",
+ )
+ }
+ return null
+ }
+
+ protected open fun queueDataFlush() {
+ dataFlushRunnable?.let { dataFlushHandler.removeCallbacks(it) }
+ dataFlushRunnable?.let { dataFlushHandler.postDelayed(it, FLUSH_DELAY.toLong()) }
+ }
+
+ /**
+ * This is called when the Kit is added to the mParticle SDK, typically on app-startup.
+ */
+ override fun setAllUserAttributes(
+ attributes: Map,
+ attributeLists: Map>,
+ ) {
+ if (!kitPreferences.getBoolean(PREF_KEY_HAS_SYNCED_ATTRIBUTES, false)) {
+ for ((key, value) in attributes) {
+ setUserAttribute(key, value)
+ }
+ for ((key, value) in attributeLists) {
+ setUserAttributeList(key, value)
+ }
+ kitPreferences.edit().putBoolean(PREF_KEY_HAS_SYNCED_ATTRIBUTES, true).apply()
+ }
+ }
+
+ override fun removeUserAttribute(keyIn: String) {
+ var key = keyIn
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ if (UserAttributes.CITY == key) {
+ value.setHomeCity(null)
+ } else if (UserAttributes.COUNTRY == key) {
+ value.setCountry(null)
+ } else if (UserAttributes.FIRSTNAME == key) {
+ value.setFirstName(null)
+ } else if (UserAttributes.LASTNAME == key) {
+ value.setLastName(null)
+ } else if (UserAttributes.MOBILE_NUMBER == key) {
+ value.setPhoneNumber(null)
+ } else {
+ if (key.startsWith("$")) {
+ key = key.substring(1)
+ }
+ value.unsetCustomUserAttribute(key)
+ }
+ queueDataFlush()
+ }
+
+ override fun onError() {
+ Logger.warning("unable to remove User Attribute with key: " + key)
+ }
+ },
+ )
+ }
+
+ override fun setUserIdentity(
+ identityType: IdentityType,
+ identity: String,
+ ) {}
+
+ override fun removeUserIdentity(identityType: IdentityType) {}
+
+ override fun logout(): List = emptyList()
+
+ fun logTransaction(
+ event: CommerceEvent?,
+ product: Product,
+ ) {
+ val purchaseProperties = BrazeProperties()
+ val currency = arrayOfNulls(1)
+ val commerceTypeParser: StringTypeParser =
+ BrazePropertiesSetter(
+ purchaseProperties,
+ enableTypeDetection,
+ )
+ val onAttributeExtracted: OnAttributeExtracted =
+ object : OnAttributeExtracted {
+ override fun onAttributeExtracted(
+ key: String,
+ value: String,
+ ) {
+ if (!checkCurrency(key, value)) {
+ commerceTypeParser.parseValue(key, value)
+ }
+ }
+
+ override fun onAttributeExtracted(
+ key: String,
+ value: Double,
+ ) {
+ if (!checkCurrency(key, value)) {
+ purchaseProperties.addProperty(key, value)
+ }
+ }
+
+ override fun onAttributeExtracted(
+ key: String,
+ value: Int,
+ ) {
+ purchaseProperties.addProperty(key, value)
+ }
+
+ override fun onAttributeExtracted(attributes: Map) {
+ for ((key, value) in attributes) {
+ if (!checkCurrency(key, value)) {
+ commerceTypeParser.parseValue(key, value)
+ }
+ }
+ }
+
+ private fun checkCurrency(
+ key: String,
+ value: Any?,
+ ): Boolean =
+ if (CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE ==
+ key
+ ) {
+ currency[0] = value?.toString()
+ true
+ } else {
+ false
+ }
+ }
+ CommerceEventUtils.extractActionAttributes(event, onAttributeExtracted)
+ var currencyValue = currency[0]
+ if (KitUtils.isEmpty(currencyValue)) {
+ currencyValue = CommerceEventUtils.Constants.DEFAULT_CURRENCY_CODE
+ }
+
+ event?.customAttributes?.let {
+ for ((key, value) in it) {
+ purchaseProperties.addProperty(key, value)
+ }
+ }
+
+ product.couponCode?.let {
+ purchaseProperties.addProperty(
+ CommerceEventUtils.Constants.ATT_PRODUCT_COUPON_CODE,
+ it,
+ )
+ }
+ product.brand?.let {
+ purchaseProperties.addProperty(CommerceEventUtils.Constants.ATT_PRODUCT_BRAND, it)
+ }
+ product.category?.let {
+ purchaseProperties.addProperty(CommerceEventUtils.Constants.ATT_PRODUCT_CATEGORY, it)
+ }
+ product.name.let {
+ purchaseProperties.addProperty(CommerceEventUtils.Constants.ATT_PRODUCT_NAME, it)
+ }
+ product.variant?.let {
+ purchaseProperties.addProperty(CommerceEventUtils.Constants.ATT_PRODUCT_VARIANT, it)
+ }
+ product.position?.let {
+ purchaseProperties.addProperty(CommerceEventUtils.Constants.ATT_PRODUCT_POSITION, it)
+ }
+ product.customAttributes?.let {
+ for ((key, value) in it) {
+ purchaseProperties.addProperty(key, value)
+ }
+ }
+
+ var sanitizedProductName: String = product.sku
+ try {
+ if (settings[REPLACE_SKU_AS_PRODUCT_NAME] == "True") {
+ sanitizedProductName = product.name
+ }
+ } catch (e: Exception) {
+ Logger.error(e, "The Braze kit threw an exception while searching for forward sku as product name flag.")
+ }
+
+ Braze.Companion.getInstance(context).logPurchase(
+ sanitizedProductName,
+ currencyValue,
+ BigDecimal(product.unitPrice),
+ product.quantity.toInt(),
+ purchaseProperties,
+ )
+ }
+
+ fun logOrderLevelTransaction(event: CommerceEvent?) {
+ val properties = BrazeProperties()
+ val currency = arrayOfNulls(1)
+ val commerceTypeParser: StringTypeParser =
+ BrazePropertiesSetter(
+ properties,
+ enableTypeDetection,
+ )
+ val onAttributeExtracted: OnAttributeExtracted =
+ object : OnAttributeExtracted {
+ override fun onAttributeExtracted(
+ key: String,
+ value: String,
+ ) {
+ if (!checkCurrency(key, value)) {
+ commerceTypeParser.parseValue(key, value)
+ }
+ }
+
+ override fun onAttributeExtracted(
+ key: String,
+ value: Double,
+ ) {
+ if (!checkCurrency(key, value)) {
+ properties.addProperty(key, value)
+ }
+ }
+
+ override fun onAttributeExtracted(
+ key: String,
+ value: Int,
+ ) {
+ properties.addProperty(key, value)
+ }
+
+ override fun onAttributeExtracted(attributes: Map) {
+ for ((key, value) in attributes) {
+ if (!checkCurrency(key, value)) {
+ commerceTypeParser.parseValue(key, value)
+ }
+ }
+ }
+
+ private fun checkCurrency(
+ key: String,
+ value: Any?,
+ ): Boolean =
+ if (CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE ==
+ key
+ ) {
+ currency[0] = value?.toString()
+ true
+ } else {
+ false
+ }
+ }
+ CommerceEventUtils.extractActionAttributes(event, onAttributeExtracted)
+ var currencyValue = currency[0]
+ if (KitUtils.isEmpty(currencyValue)) {
+ currencyValue = CommerceEventUtils.Constants.DEFAULT_CURRENCY_CODE
+ }
+
+ event?.customAttributes?.let {
+ properties.addProperty(CUSTOM_ATTRIBUTES_KEY, it)
+ }
+
+ val productList = event?.products
+ productList?.let {
+ val productArray = getProductListParameters(it)
+ properties.addProperty(PRODUCT_KEY, productArray)
+ }
+
+ val promotionList = event?.promotions
+ promotionList?.let {
+ val promotionArray = getPromotionListParameters(it)
+ properties.addProperty(PROMOTION_KEY, promotionArray)
+ }
+
+ val impressionList = event?.impressions
+ impressionList?.let {
+ val impressionArray = getImpressionListParameters(it)
+ properties.addProperty(IMPRESSION_KEY, impressionArray)
+ }
+
+ val eventName = "eCommerce - %s"
+ if (!KitUtils.isEmpty(event?.productAction) &&
+ event?.productAction.equals(Product.PURCHASE, true)
+ ) {
+ Braze.Companion.getInstance(context).logPurchase(
+ String.format(eventName, event?.productAction),
+ currencyValue,
+ event?.transactionAttributes?.revenue?.let { BigDecimal(it) } ?: BigDecimal(0),
+ 1,
+ properties,
+ )
+ } else {
+ if (!KitUtils.isEmpty(event?.productAction)) {
+ Braze
+ .getInstance(context)
+ .logCustomEvent(
+ String.format(eventName, event?.productAction),
+ properties,
+ )
+ } else if (!KitUtils.isEmpty(event?.promotionAction)) {
+ Braze
+ .getInstance(context)
+ .logCustomEvent(
+ String.format(eventName, event?.promotionAction),
+ properties,
+ )
+ } else {
+ Braze
+ .getInstance(context)
+ .logCustomEvent(
+ String.format(eventName, "Impression"),
+ properties,
+ )
+ }
+ }
+ }
+
+ override fun willHandlePushMessage(intent: Intent): Boolean =
+ if (!(settings[PUSH_ENABLED].toBoolean())) {
+ false
+ } else {
+ intent.isBrazePushMessage()
+ }
+
+ override fun onPushMessageReceived(
+ context: Context,
+ pushIntent: Intent,
+ ) {
+ if (settings[PUSH_ENABLED].toBoolean()) {
+ BrazeFirebaseMessagingService.handleBrazeRemoteMessage(
+ context,
+ RemoteMessage(pushIntent.extras),
+ )
+ }
+ }
+
+ override fun onPushRegistration(
+ instanceId: String,
+ senderId: String,
+ ): Boolean =
+ if (settings[PUSH_ENABLED].toBoolean()) {
+ updatedInstanceId = instanceId
+ Braze.getInstance(context).registeredPushToken = instanceId
+ queueDataFlush()
+ true
+ } else {
+ false
+ }
+
+ protected open fun setAuthority(authority: String?) {
+ Braze.setEndpointProvider { appboyEndpoint ->
+ appboyEndpoint
+ .buildUpon()
+ .authority(authority)
+ .build()
+ }
+ }
+
+ override fun onIdentifyCompleted(
+ mParticleUser: MParticleUser,
+ filteredIdentityApiRequest: FilteredIdentityApiRequest?,
+ ) {
+ updateUser(mParticleUser)
+ }
+
+ override fun onLoginCompleted(
+ mParticleUser: MParticleUser,
+ filteredIdentityApiRequest: FilteredIdentityApiRequest?,
+ ) {
+ updateUser(mParticleUser)
+ }
+
+ override fun onLogoutCompleted(
+ mParticleUser: MParticleUser,
+ filteredIdentityApiRequest: FilteredIdentityApiRequest?,
+ ) {
+ updateUser(mParticleUser)
+ }
+
+ override fun onModifyCompleted(
+ mParticleUser: MParticleUser,
+ filteredIdentityApiRequest: FilteredIdentityApiRequest?,
+ ) {
+ updateUser(mParticleUser)
+ }
+
+ private fun updateUser(mParticleUser: MParticleUser) {
+ val identity = getIdentity(isMpidIdentityType, identityType, mParticleUser)
+ val email = mParticleUser.userIdentities[IdentityType.Email]
+ identity?.let { setId(it) }
+ email?.let { setEmail(it) }
+ }
+
+ fun getIdentity(
+ isMpidIdentityType: Boolean,
+ identityType: IdentityType?,
+ mParticleUser: MParticleUser?,
+ ): String? {
+ var identity: String? = null
+ if (isMpidIdentityType && mParticleUser != null) {
+ identity = mParticleUser.id.toString()
+ } else if (identityType != null && mParticleUser != null) {
+ identity = mParticleUser.userIdentities[identityType]
+ }
+ return identity
+ }
+
+ protected open fun setId(customerId: String) {
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ if (value.userId != customerId) {
+ Braze.getInstance(context).changeUser(customerId)
+ queueDataFlush()
+ }
+ }
+
+ override fun onError() {
+ Logger.warning("unable to change user to customer ID: " + customerId)
+ }
+ },
+ )
+ }
+
+ protected open fun setEmail(email: String) {
+ if (email != kitPreferences.getString(PREF_KEY_CURRENT_EMAIL, null)) {
+ Braze
+ .getInstance(context)
+ .getCurrentUser(
+ object : IValueCallback {
+ override fun onSuccess(value: BrazeUser) {
+ value.setEmail(email)
+ queueDataFlush()
+ kitPreferences
+ .edit()
+ .putString(PREF_KEY_CURRENT_EMAIL, email)
+ .apply()
+ }
+
+ override fun onError() {
+ Logger.warning("unable to set email with value: " + email)
+ }
+ },
+ )
+ }
+ }
+
+ override fun onUserIdentified(mParticleUser: MParticleUser) {
+ if (updatedInstanceId.isNotEmpty()) {
+ Braze.getInstance(context).registeredPushToken = updatedInstanceId
+ }
+ }
+
+ fun addToProperties(
+ properties: BrazeProperties,
+ key: String,
+ value: String,
+ ) {
+ try {
+ if ("true".equals(value, true) ||
+ "false".equals(
+ value,
+ true,
+ )
+ ) {
+ properties.addProperty(key, (value).toBoolean())
+ } else {
+ val doubleValue = value.toDouble()
+ if (doubleValue % 1 == 0.0) {
+ properties.addProperty(key, value.toInt())
+ } else {
+ properties.addProperty(key, doubleValue)
+ }
+ }
+ } catch (e: Exception) {
+ properties.addProperty(key, value)
+ }
+ }
+
+ fun getCalendarMinusYears(yearsString: String): Calendar? {
+ try {
+ val years = yearsString.toInt()
+ return getCalendarMinusYears(years)
+ } catch (ignored: NumberFormatException) {
+ try {
+ val years = yearsString.toDouble()
+ return getCalendarMinusYears(years.toInt())
+ } catch (ignoredToo: NumberFormatException) {
+ }
+ }
+ return null
+ }
+
+ fun getCalendarMinusYears(years: Int): Calendar? =
+ if (years >= 0) {
+ val calendar = Calendar.getInstance()
+ calendar[Calendar.YEAR] = calendar[Calendar.YEAR] - years
+ calendar
+ } else {
+ null
+ }
+
+ fun getProductListParameters(productList: List): JSONArray {
+ val productArray = JSONArray()
+ for ((i, product) in productList.withIndex()) {
+ val productProperties = JSONObject()
+
+ product.customAttributes?.let {
+ productProperties.put(CUSTOM_ATTRIBUTES_KEY, it)
+ }
+ product.couponCode?.let {
+ productProperties.put(
+ CommerceEventUtils.Constants.ATT_PRODUCT_COUPON_CODE,
+ it,
+ )
+ }
+ product.brand?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_BRAND, it)
+ }
+ product.category?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_CATEGORY, it)
+ }
+ product.name?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_NAME, it)
+ }
+ product.sku?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_ID, it)
+ }
+ product.variant?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_VARIANT, it)
+ }
+ product.position?.let {
+ productProperties.put(CommerceEventUtils.Constants.ATT_PRODUCT_POSITION, it)
+ }
+ productProperties.put(
+ CommerceEventUtils.Constants.ATT_PRODUCT_PRICE,
+ product.unitPrice,
+ )
+ productProperties.put(
+ CommerceEventUtils.Constants.ATT_PRODUCT_QUANTITY,
+ product.quantity,
+ )
+ productProperties.put(
+ CommerceEventUtils.Constants.ATT_PRODUCT_TOTAL_AMOUNT,
+ product.totalAmount,
+ )
+
+ productArray.put(productProperties)
+ }
+ return productArray
+ }
+
+ fun getPromotionListParameters(promotionList: List): JSONArray {
+ val promotionArray = JSONArray()
+ for ((i, promotion) in promotionList.withIndex()) {
+ val promotionProperties = JSONObject()
+ promotion.creative?.let {
+ promotionProperties.put(
+ CommerceEventUtils.Constants.ATT_PROMOTION_CREATIVE,
+ it,
+ )
+ }
+ promotion.id?.let {
+ promotionProperties.put(CommerceEventUtils.Constants.ATT_PROMOTION_ID, it)
+ }
+ promotion.name?.let {
+ promotionProperties.put(CommerceEventUtils.Constants.ATT_PROMOTION_NAME, it)
+ }
+ promotion.position?.let {
+ promotionProperties.put(
+ CommerceEventUtils.Constants.ATT_PROMOTION_POSITION,
+ it,
+ )
+ }
+ promotionArray.put(promotionProperties)
+ }
+ return promotionArray
+ }
+
+ private fun getSubscriptionGroupIds(subscriptionGroupMap: String): MutableMap {
+ val subscriptionGroupIds = mutableMapOf()
+
+ if (subscriptionGroupMap.isEmpty()) {
+ return subscriptionGroupIds
+ }
+
+ val subscriptionGroupsArray = JSONArray(subscriptionGroupMap)
+
+ return try {
+ for (i in 0 until subscriptionGroupsArray.length()) {
+ val subscriptionGroup = subscriptionGroupsArray.getJSONObject(i)
+ val key = subscriptionGroup.getString("map")
+ val value = subscriptionGroup.getString("value")
+ subscriptionGroupIds[key] = value
+ }
+ subscriptionGroupIds
+ } catch (e: JSONException) {
+ Logger.warning("Braze, unable to parse \"subscriptionGroup\"")
+ mutableMapOf()
+ }
+ }
+
+ fun getImpressionListParameters(impressionList: List): JSONArray {
+ val impressionArray = JSONArray()
+ for ((i, impression) in impressionList.withIndex()) {
+ val impressionProperties = JSONObject()
+ impression.listName?.let {
+ impressionProperties.put("Product Impression List", it)
+ }
+ impression.products?.let {
+ val productArray = getProductListParameters(it)
+ impressionProperties.put(PRODUCT_KEY, productArray)
+ }
+ impressionArray.put(impressionProperties)
+ }
+ return impressionArray
+ }
+
+ internal abstract class StringTypeParser(
+ var enableTypeDetection: Boolean,
+ ) {
+ fun parseValue(
+ key: String,
+ value: String,
+ ): Any {
+ if (!enableTypeDetection) {
+ toString(key, value)
+ return value
+ }
+ return if (
+ true.toString().equals(value, true) ||
+ false.toString().equals(
+ value,
+ true,
+ )
+ ) {
+ val newBool = (value).toBoolean()
+ toBoolean(key, newBool)
+ newBool
+ } else {
+ try {
+ if (value.contains(".")) {
+ val doubleValue = value.toDouble()
+ toDouble(key, doubleValue)
+ doubleValue
+ } else {
+ val newLong = value.toLong()
+ if (newLong <= Int.MAX_VALUE && newLong >= Int.MIN_VALUE) {
+ val newInt = newLong.toInt()
+ toInt(key, newInt)
+ newInt
+ } else {
+ toLong(key, newLong)
+ newLong
+ }
+ }
+ } catch (nfe: NumberFormatException) {
+ toString(key, value)
+ value
+ }
+ }
+ }
+
+ abstract fun toInt(
+ key: String,
+ value: Int,
+ )
+
+ abstract fun toLong(
+ key: String,
+ value: Long,
+ )
+
+ abstract fun toDouble(
+ key: String,
+ value: Double,
+ )
+
+ abstract fun toBoolean(
+ key: String,
+ value: Boolean,
+ )
+
+ abstract fun toString(
+ key: String,
+ value: String,
+ )
+ }
+
+ internal inner class BrazePropertiesSetter(
+ private var properties: BrazeProperties,
+ enableTypeDetection: Boolean,
+ ) : StringTypeParser(enableTypeDetection) {
+ override fun toInt(
+ key: String,
+ value: Int,
+ ) {
+ properties.addProperty(key, value)
+ }
+
+ override fun toLong(
+ key: String,
+ value: Long,
+ ) {
+ properties.addProperty(key, value)
+ }
+
+ override fun toDouble(
+ key: String,
+ value: Double,
+ ) {
+ properties.addProperty(key, value)
+ }
+
+ override fun toBoolean(
+ key: String,
+ value: Boolean,
+ ) {
+ properties.addProperty(key, value)
+ }
+
+ override fun toString(
+ key: String,
+ value: String,
+ ) {
+ properties.addProperty(key, value)
+ }
+ }
+
+ internal inner class UserAttributeSetter(
+ private var brazeUser: BrazeUser,
+ enableTypeDetection: Boolean,
+ ) : StringTypeParser(enableTypeDetection) {
+ override fun toInt(
+ key: String,
+ value: Int,
+ ) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+
+ override fun toLong(
+ key: String,
+ value: Long,
+ ) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+
+ override fun toDouble(
+ key: String,
+ value: Double,
+ ) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+
+ override fun toBoolean(
+ key: String,
+ value: Boolean,
+ ) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+
+ override fun toString(
+ key: String,
+ value: String,
+ ) {
+ brazeUser.setCustomUserAttribute(key, value)
+ }
+ }
+
+ companion object {
+ const val APPBOY_KEY = "apiKey"
+ const val FORWARD_SCREEN_VIEWS = "forwardScreenViews"
+ const val USER_IDENTIFICATION_TYPE = "userIdentificationType"
+ const val ENABLE_TYPE_DETECTION = "enableTypeDetection"
+ const val BUNDLE_COMMERCE_EVENTS = "bundleCommerceEventData"
+ const val SUBSCRIPTION_GROUP_MAPPING = "subscriptionGroupMapping"
+ const val HOST = "host"
+ const val PUSH_ENABLED = "push_enabled"
+ const val NAME = "Appboy"
+
+ // if this flag is true, kit will send Product name as sku
+ const val REPLACE_SKU_AS_PRODUCT_NAME = "replaceSkuWithProductName"
+ private const val PREF_KEY_HAS_SYNCED_ATTRIBUTES = "appboy::has_synced_attributes"
+ private const val PREF_KEY_CURRENT_EMAIL = "appboy::current_email"
+ private const val FLUSH_DELAY = 5000
+ var setDefaultAppboyLifecycleCallbackListener = true
+
+ private const val EMAIL_SUBSCRIBE = "email_subscribe"
+ private const val PUSH_SUBSCRIBE = "push_subscribe"
+ private const val DOB = "dob"
+
+ private const val OPTED_IN = "opted_in"
+ private const val UNSUBSCRIBED = "unsubscribed"
+ private const val SUBSCRIBED = "subscribed"
+
+ // Constants for Read Consent
+ private const val CONSENT_MAPPING_SDK = "consentMappingSDK"
+ private const val KEY_GOOGLE_AD_USER_DATA = "\$google_ad_user_data"
+ private const val KEY_GOOGLE_AD_PERSONALIZATION = "\$google_ad_personalization"
+
+ const val CUSTOM_ATTRIBUTES_KEY = "Attributes"
+ const val PRODUCT_KEY = "products"
+ const val PROMOTION_KEY = "promotions"
+ const val IMPRESSION_KEY = "impressions"
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/braze/Braze.kt b/kits/braze/braze-39/src/test/kotlin/com/braze/Braze.kt
new file mode 100644
index 000000000..f2ebeffe7
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/braze/Braze.kt
@@ -0,0 +1,63 @@
+package com.braze
+
+import android.content.Context
+import com.braze.configuration.BrazeConfig
+import com.braze.events.IValueCallback
+import com.braze.models.outgoing.BrazeProperties
+import com.mparticle.kits.BrazePurchase
+import java.math.BigDecimal
+
+class Braze {
+ fun getCurrentUser(): BrazeUser = Companion.currentUser
+
+ fun getCustomAttributeArray(): java.util.HashMap> = Companion.currentUser.getCustomAttribute()
+
+ fun getCurrentUser(callback: IValueCallback) {
+ callback.onSuccess(currentUser)
+ }
+
+ fun logCustomEvent(
+ key: String,
+ brazeProperties: BrazeProperties,
+ ) {
+ events[key] = brazeProperties
+ }
+
+ fun logCustomEvent(key: String) {
+ events[key] = BrazeProperties()
+ }
+
+ fun logPurchase(
+ sku: String,
+ currency: String,
+ unitPrice: BigDecimal,
+ quantity: Int,
+ purchaseProperties: BrazeProperties,
+ ) {
+ purchases.add(BrazePurchase(sku, currency, unitPrice, quantity, purchaseProperties))
+ }
+
+ companion object {
+ val purchases: MutableList = ArrayList()
+ val events: MutableMap = HashMap()
+
+ fun clearPurchases() {
+ purchases.clear()
+ }
+
+ fun clearEvents() {
+ events.clear()
+ }
+
+ val currentUser = BrazeUser()
+
+ @JvmStatic
+ fun configure(
+ context: Context?,
+ config: BrazeConfig?,
+ ) = true
+
+ @JvmStatic
+ fun getInstance(context: Context?): Braze = Braze()
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/braze/BrazeUser.kt b/kits/braze/braze-39/src/test/kotlin/com/braze/BrazeUser.kt
new file mode 100644
index 000000000..13983911e
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/braze/BrazeUser.kt
@@ -0,0 +1,95 @@
+package com.braze
+
+import com.braze.enums.Month
+
+class BrazeUser {
+ var dobYear = -1
+ var dobMonth: Month? = null
+ var dobDay = -1
+
+ fun setDateOfBirth(
+ year: Int,
+ month: Month?,
+ day: Int,
+ ): Boolean {
+ dobYear = year
+ dobMonth = month
+ dobDay = day
+ return true
+ }
+
+ val customAttributeArray = HashMap>()
+ val customUserAttributes = HashMap()
+
+ fun addToCustomAttributeArray(
+ key: String,
+ value: String,
+ ): Boolean {
+ var customArray = customAttributeArray[key]
+ if (customArray == null) {
+ customArray = ArrayList()
+ }
+ customArray.add(value)
+ customAttributeArray[key] = customArray
+ return true
+ }
+
+ fun removeFromCustomAttributeArray(
+ key: String,
+ value: String,
+ ): Boolean =
+ try {
+ if (customAttributeArray.containsKey(key)) {
+ customAttributeArray.remove(key)
+ }
+ true
+ } catch (npe: NullPointerException) {
+ false
+ }
+
+ fun setCustomUserAttribute(
+ key: String,
+ value: String,
+ ): Boolean {
+ customUserAttributes[key] = value
+ return true
+ }
+
+ fun setCustomUserAttribute(
+ key: String,
+ value: Boolean,
+ ): Boolean {
+ customUserAttributes[key] = value
+ return true
+ }
+
+ fun setCustomUserAttribute(
+ key: String,
+ value: Int,
+ ): Boolean {
+ customUserAttributes[key] = value
+ return true
+ }
+
+ fun setCustomUserAttribute(
+ key: String,
+ value: Double,
+ ): Boolean {
+ customUserAttributes[key] = value
+ return true
+ }
+
+ fun addToSubscriptionGroup(key: String): Boolean {
+ customUserAttributes[key] = true
+ return true
+ }
+
+ fun removeFromSubscriptionGroup(key: String): Boolean {
+ customUserAttributes[key] = false
+ return true
+ }
+
+ fun getCustomAttribute(): HashMap> = customAttributeArray
+
+ fun getCustomUserAttribute(): HashMap = customUserAttributes
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/braze/models/outgoing/BrazeProperties.kt b/kits/braze/braze-39/src/test/kotlin/com/braze/models/outgoing/BrazeProperties.kt
new file mode 100644
index 000000000..e36de6578
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/braze/models/outgoing/BrazeProperties.kt
@@ -0,0 +1,55 @@
+package com.braze.models.outgoing
+
+import java.util.HashMap
+
+class BrazeProperties {
+ val properties = HashMap()
+
+ fun addProperty(
+ key: String,
+ value: Long,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+
+ fun addProperty(
+ key: String,
+ value: Int,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+
+ fun addProperty(
+ key: String,
+ value: String,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+
+ fun addProperty(
+ key: String,
+ value: Double,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+
+ fun addProperty(
+ key: String,
+ value: Boolean,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+
+ fun addProperty(
+ key: String,
+ value: Any,
+ ): BrazeProperties {
+ properties[key] = value
+ return this
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/AppboyKitTests.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/AppboyKitTests.kt
new file mode 100644
index 000000000..bbcf6419b
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/AppboyKitTests.kt
@@ -0,0 +1,1557 @@
+package com.mparticle.kits
+
+import android.util.SparseBooleanArray
+import com.braze.Braze
+import com.braze.models.outgoing.BrazeProperties
+import com.mparticle.MPEvent
+import com.mparticle.MParticle
+import com.mparticle.MParticle.IdentityType
+import com.mparticle.MParticleOptions
+import com.mparticle.commerce.CommerceEvent
+import com.mparticle.commerce.Impression
+import com.mparticle.commerce.Product
+import com.mparticle.commerce.Promotion
+import com.mparticle.commerce.TransactionAttributes
+import com.mparticle.consent.ConsentState
+import com.mparticle.consent.GDPRConsent
+import com.mparticle.identity.IdentityApi
+import com.mparticle.identity.MParticleUser
+import com.mparticle.kits.mocks.MockAppboyKit
+import com.mparticle.kits.mocks.MockContextApplication
+import com.mparticle.kits.mocks.MockKitConfiguration
+import com.mparticle.kits.mocks.MockUser
+import junit.framework.TestCase
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import java.lang.reflect.Method
+import java.math.BigDecimal
+import java.security.SecureRandom
+import java.util.Calendar
+import java.util.Locale
+
+class AppboyKitTests {
+ private var random = SecureRandom()
+
+ private lateinit var braze: Braze.Companion
+
+ @Mock
+ private val mTypeFilters: SparseBooleanArray? = null
+
+ @Mock
+ lateinit var filteredMParticleUser: FilteredMParticleUser
+
+ @Mock
+ lateinit var user: MParticleUser
+
+ private val kit: AppboyKit
+ get() = AppboyKit()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ Braze.clearPurchases()
+ Braze.clearEvents()
+ Braze.currentUser.customUserAttributes.clear()
+ MParticle.setInstance(Mockito.mock(MParticle::class.java))
+ Mockito.`when`(MParticle.getInstance()!!.Identity()).thenReturn(
+ Mockito.mock(
+ IdentityApi::class.java,
+ ),
+ )
+ braze = Braze
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testGetName() {
+ val name = kit.name
+ Assert.assertTrue(name.isNotEmpty())
+ }
+
+ /**
+ * Kit *should* throw an exception when they're initialized with the wrong settings.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testOnKitCreate() {
+ var e: Exception? = null
+ try {
+ val kit: KitIntegration = kit
+ val settings = HashMap()
+ settings["fake setting"] = "fake"
+ kit.onKitCreate(settings, MockContextApplication())
+ } catch (ex: Exception) {
+ e = ex
+ }
+ Assert.assertNotNull(e)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testClassName() {
+ val options = Mockito.mock(MParticleOptions::class.java)
+ val factory = KitIntegrationFactory(options)
+ val integrations = factory.supportedKits.values
+ val className = kit.javaClass.name
+ for (integration in integrations) {
+ if (integration.name == className) {
+ return
+ }
+ }
+ Assert.fail("$className not found as a known integration.")
+ }
+
+ private var hostName = "aRandomHost"
+
+ @Test
+ @Throws(Exception::class)
+ fun testHostSetting() {
+ val settings = HashMap()
+ settings[AppboyKit.HOST] = hostName
+ settings[AppboyKit.APPBOY_KEY] = "key"
+ val kit = MockAppboyKit()
+ kit.onKitCreate(settings, MockContextApplication())
+ Assert.assertTrue(kit.calledAuthority[0] == hostName)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testHostSettingNull() {
+ // test that the key is set when it is passed in by the settings map
+ val missingSettings = HashMap()
+ missingSettings[AppboyKit.APPBOY_KEY] = "key"
+ val kit = MockAppboyKit()
+ try {
+ kit.onKitCreate(missingSettings, MockContextApplication())
+ } catch (e: Exception) {
+ }
+ Assert.assertTrue(kit.calledAuthority[0] == null)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testHostSettingEmpty() {
+ var nullSettings = HashMap()
+ nullSettings[AppboyKit.HOST] = null
+ nullSettings[AppboyKit.APPBOY_KEY] = "key"
+ var kit = MockAppboyKit()
+ try {
+ kit.onKitCreate(nullSettings, MockContextApplication())
+ } catch (e: Exception) {
+ }
+ Assert.assertTrue(kit.calledAuthority[0] == null)
+ nullSettings = HashMap()
+ nullSettings[AppboyKit.HOST] = ""
+ nullSettings[AppboyKit.APPBOY_KEY] = "key"
+ kit = MockAppboyKit()
+ try {
+ kit.onKitCreate(nullSettings, MockContextApplication())
+ } catch (e: Exception) {
+ }
+ Assert.assertTrue(kit.calledAuthority[0] == null)
+ }
+
+ @Test
+ fun testOnModify() {
+ // make sure it doesn't crash if there is no email or customerId
+ var e: Exception? = null
+ try {
+ AppboyKit().onModifyCompleted(MockUser(HashMap()), null)
+ } catch (ex: Exception) {
+ e = ex
+ }
+ Assert.assertNull(e)
+ for (i in 0..3) {
+ val values = arrayOfNulls(2)
+ val mockEmail = "mockEmail$i"
+ val mockCustomerId = "12345$i"
+ val kit: AppboyKit =
+ object : AppboyKit() {
+ override fun setId(customerId: String) {
+ values[0] = customerId
+ }
+
+ override fun setEmail(email: String) {
+ if (values[0] == null) {
+ Assert.fail("customerId should have been set first")
+ }
+ values[1] = email
+ }
+ }
+ kit.identityType = IdentityType.CustomerId
+ val map = HashMap()
+ map[IdentityType.Email] = mockEmail
+ map[IdentityType.Alias] = "alias"
+ map[IdentityType.Facebook] = "facebook"
+ map[IdentityType.Facebook] = "fb"
+ map[IdentityType.CustomerId] = mockCustomerId
+ when (i) {
+ 0 -> {
+ kit.onModifyCompleted(MockUser(map), null)
+ kit.onIdentifyCompleted(MockUser(map), null)
+ kit.onLoginCompleted(MockUser(map), null)
+ kit.onLogoutCompleted(MockUser(map), null)
+ }
+ 1 -> {
+ kit.onIdentifyCompleted(MockUser(map), null)
+ kit.onLoginCompleted(MockUser(map), null)
+ kit.onLogoutCompleted(MockUser(map), null)
+ }
+ 2 -> {
+ kit.onLoginCompleted(MockUser(map), null)
+ kit.onLogoutCompleted(MockUser(map), null)
+ }
+ 3 -> kit.onLogoutCompleted(MockUser(map), null)
+ }
+ Assert.assertEquals(mockCustomerId, values[0])
+ Assert.assertEquals(mockEmail, values[1])
+ }
+ }
+
+ @Test
+ fun testAgeToDob() {
+ val kit: AppboyKit = MockAppboyKit()
+ val currentYear = Calendar.getInstance()[Calendar.YEAR]
+ var calendar = kit.getCalendarMinusYears("5")
+ calendar
+ ?.get(Calendar.YEAR)
+ ?.let { Assert.assertEquals((currentYear - 5).toLong(), it.toLong()) }
+ calendar = kit.getCalendarMinusYears(22)
+ calendar
+ ?.get(Calendar.YEAR)
+ ?.let { Assert.assertEquals((currentYear - 22).toLong(), it.toLong()) }
+
+// round down doubles
+ calendar = kit.getCalendarMinusYears("5.001")
+ calendar
+ ?.get(Calendar.YEAR)
+ ?.let { Assert.assertEquals((currentYear - 5).toLong(), it.toLong()) }
+ calendar = kit.getCalendarMinusYears("5.9")
+ calendar
+ ?.get(Calendar.YEAR)
+ ?.let { Assert.assertEquals((currentYear - 5).toLong(), it.toLong()) }
+
+ // invalid ages (negative, non numeric), don't get set
+ Assert.assertNull(kit.getCalendarMinusYears("asdv"))
+ Assert.assertNull(kit.getCalendarMinusYears(-1))
+ }
+
+ @Test
+ fun testSetSubscriptionGroupIds() {
+ val settings = HashMap()
+ settings[AppboyKit.APPBOY_KEY] = "key"
+ settings[AppboyKit.HOST] = hostName
+ settings["subscriptionGroupMapping"] =
+ "[{\"jsmap\":null,\"map\":\"test1\",\"maptype\":\"UserAttributeClass.Name\"," +
+ "\"value\":\"00000000-0000-0000-0000-000000000000\"}," +
+ "{\"jsmap\":null,\"map\":\"test2\",\"maptype\":\"UserAttributeClass.Name\"," +
+ "\"value\":\"00000000-0000-0000-0000-000000000001\"}," +
+ "{\"jsmap\":null,\"map\":\"test3\",\"maptype\":\"UserAttributeClass.Name\"," +
+ "\"value\":\"00000000-0000-0000-0000-000000000002\"}]"
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+
+ kit.onKitCreate(settings, MockContextApplication())
+ kit.setUserAttribute("test1", "true")
+ kit.setUserAttribute("test2", "false")
+ kit.setUserAttribute("test3", "notABoolean")
+ Assert.assertEquals(2, currentUser.getCustomUserAttribute().size.toLong())
+ }
+
+// @Test
+// fun testSetUserAttributeAge() {
+// val currentYear = Calendar.getInstance()[Calendar.YEAR]
+// val kit: AppboyKit = MockAppboyKit()
+// val currentUser = Braze.currentUser
+// Assert.assertEquals(-1, currentUser.dobDay.toLong())
+// Assert.assertEquals(-1, currentUser.dobYear.toLong())
+// Assert.assertNull(currentUser.dobMonth)
+// kit.setUserAttribute(MParticle.UserAttributes.AGE, "100")
+// Assert.assertEquals((currentYear - 100).toLong(), currentUser.dobYear.toLong())
+// Assert.assertEquals(1, currentUser.dobDay.toLong())
+// Assert.assertEquals(Month.JANUARY, currentUser.dobMonth)
+// }
+
+// @Test
+// fun testSetUserDoB() {
+// val kit = MockAppboyKit()
+// val currentUser = Braze.currentUser
+// val errorMessage = arrayOfNulls(1)
+// Logger.setLogHandler(object : DefaultLogHandler() {
+// override fun log(priority: LogLevel, error: Throwable?, messages: String) {
+// if (priority == LogLevel.WARNING) {
+// errorMessage[0] = messages
+// }
+// }
+// })
+//
+// //valid
+// kit.setUserAttribute("dob", "1999-11-05")
+// Assert.assertEquals(1999, currentUser.dobYear.toLong())
+// Assert.assertEquals(5, currentUser.dobDay.toLong())
+// Assert.assertEquals(Month.NOVEMBER, currentUser.dobMonth)
+// Assert.assertNull(errorMessage[0])
+//
+// //future
+// kit.setUserAttribute("dob", "2999-2-15")
+// Assert.assertEquals(2999, currentUser.dobYear.toLong())
+// Assert.assertEquals(15, currentUser.dobDay.toLong())
+// Assert.assertEquals(Month.FEBRUARY, currentUser.dobMonth)
+// Assert.assertNull(errorMessage[0])
+//
+//
+// //bad format (shouldn't crash, but should message)
+// var ex: Exception? = null
+// try {
+// kit.setUserAttribute("dob", "2kjb.21h045")
+// Assert.assertEquals(2999, currentUser.dobYear.toLong())
+// Assert.assertEquals(15, currentUser.dobDay.toLong())
+// Assert.assertEquals(Month.FEBRUARY, currentUser.dobMonth)
+// Assert.assertNotNull(errorMessage[0])
+// } catch (e: Exception) {
+// ex = e
+// }
+// Assert.assertNull(ex)
+// }
+
+ @Test
+ fun setIdentityType() {
+ val possibleValues =
+ arrayOf(
+ "Other",
+ "CustomerId",
+ "Facebook",
+ "Twitter",
+ "Google",
+ "Microsoft",
+ "Yahoo",
+ "Email",
+ "Alias",
+ )
+ val mpid = "MPID"
+ for (`val` in possibleValues) {
+ val kit = kit
+ val settings = HashMap()
+ settings[AppboyKit.USER_IDENTIFICATION_TYPE] = `val`
+ kit.setIdentityType(settings)
+ Assert.assertNotNull(kit.identityType)
+ Assert.assertEquals(
+ `val`.lowercase(Locale.getDefault()),
+ kit.identityType?.name?.lowercase(Locale.getDefault()),
+ )
+ Assert.assertFalse(kit.isMpidIdentityType)
+ }
+ val settings = HashMap()
+ settings[AppboyKit.USER_IDENTIFICATION_TYPE] = mpid
+ val kit = kit
+ kit.setIdentityType(settings)
+ Assert.assertNull(kit.identityType)
+ Assert.assertTrue(kit.isMpidIdentityType)
+ }
+
+ @Test
+ fun setId() {
+ val userIdentities = HashMap()
+ val user = Mockito.mock(MParticleUser::class.java)
+ Mockito.`when`(user.userIdentities).thenReturn(userIdentities)
+ val mockId = random.nextLong()
+ Mockito.`when`(user.id).thenReturn(mockId)
+ Assert.assertEquals(mockId.toString(), kit.getIdentity(true, null, user))
+ for (identityType in IdentityType.values()) {
+ val identityValue = random.nextLong().toString()
+ userIdentities[identityType] = identityValue
+ Assert.assertEquals(identityValue, kit.getIdentity(false, identityType, user))
+ }
+ Assert.assertNull(kit.getIdentity(false, null, null))
+ }
+
+// @Test
+// fun addRemoveAttributeFromEventTest() {
+// val kit = MockAppboyKit()
+// val currentUser = Braze.currentUser
+// kit.configuration = object : MockKitConfiguration() {
+//
+// override fun getEventAttributesAddToUser(): Map {
+// val map = HashMap()
+// map[KitUtils.hashForFiltering(
+// MParticle.EventType.Navigation.toString() + "Navigation Event" + "key1"
+// )] = "output"
+// return map
+// }
+//
+// override fun getEventAttributesRemoveFromUser(): Map {
+// val map = HashMap()
+// map[KitUtils.hashForFiltering(
+// MParticle.EventType.Location.toString() + "location event" + "key1"
+// )] = "output"
+// return map
+// }
+//
+// }
+// val customAttributes = HashMap()
+// customAttributes["key1"] = "value1"
+// kit.logEvent(
+// MPEvent.Builder("Navigation Event", MParticle.EventType.Navigation)
+// .customAttributes(customAttributes)
+// .build()
+// )
+// var attributes = currentUser.customAttributeArray["output"]
+// if (attributes != null) {
+// Assert.assertEquals(1, attributes.size)
+// Assert.assertEquals("value1", attributes[0])
+// }
+// kit.logEvent(
+// MPEvent.Builder("location event", MParticle.EventType.Location)
+// .customAttributes(customAttributes)
+// .build()
+// )
+// attributes = currentUser.customAttributeArray["output"]
+//
+// if (attributes != null) {
+// Assert.assertEquals(0, attributes.size)
+// }
+// }
+
+ @Test
+ fun testPurchaseCurrency() {
+ val kit = MockAppboyKit()
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.CHECKOUT, product)
+ .currency("Moon Dollars")
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("Moon Dollars", purchase.currency)
+ Assert.assertNull(
+ purchase.purchaseProperties.properties[CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE],
+ )
+ }
+
+ @Test
+ fun testPurchaseDefaultCurrency() {
+ val kit = MockAppboyKit()
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.CHECKOUT, product)
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals(CommerceEventUtils.Constants.DEFAULT_CURRENCY_CODE, purchase.currency)
+ Assert.assertNull(
+ purchase.purchaseProperties.properties[CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE],
+ )
+ }
+
+ @Test
+ fun testPurchase() {
+ val kit = MockAppboyKit()
+ val customAttributes = HashMap()
+ customAttributes["key1"] = "value1"
+ customAttributes["key #2"] = "value #3"
+ val productCustomAttributes = HashMap()
+ productCustomAttributes["productKey1"] = "value1"
+ productCustomAttributes["productKey2"] = "value2"
+ val transactionAttributes =
+ TransactionAttributes("the id")
+ .setTax(100.0)
+ .setShipping(12.0)
+ .setRevenue(99.0)
+ .setCouponCode("coupon code")
+ .setAffiliation("the affiliation")
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .quantity(5.0)
+ .brand("testBrand")
+ .variant("testVariant")
+ .position(1)
+ .category("testCategory")
+ .customAttributes(productCustomAttributes)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.PURCHASE, product)
+ .currency("Moon Dollars")
+ .productListName("product list name")
+ .productListSource("the source")
+ .customAttributes(customAttributes)
+ .transactionAttributes(transactionAttributes)
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("Moon Dollars", purchase.currency)
+ Assert.assertEquals(5.0, purchase.quantity.toDouble(), 0.01)
+ Assert.assertEquals("sku1", purchase.sku)
+ Assert.assertEquals(BigDecimal(4.5), purchase.unitPrice)
+ Assert.assertNotNull(purchase.purchaseProperties)
+ val properties = purchase.purchaseProperties.properties
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_SHIPPING), 12.0)
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_ACTION_PRODUCT_LIST_SOURCE),
+ "the source",
+ )
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TAX), 100.0)
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TOTAL), 99.0)
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_ACTION_PRODUCT_ACTION_LIST),
+ "product list name",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_COUPON_CODE),
+ "coupon code",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_TRANSACTION_ID),
+ "the id",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_AFFILIATION),
+ "the affiliation",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_NAME),
+ "product name",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_CATEGORY),
+ "testCategory",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_BRAND),
+ "testBrand",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_POSITION),
+ 1,
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_VARIANT),
+ "testVariant",
+ )
+
+ // Custom Attributes
+ Assert.assertEquals(properties.remove("key1"), "value1")
+ Assert.assertEquals(properties.remove("key #2"), "value #3")
+
+ // Product Custom Attributes
+ Assert.assertEquals(properties.remove("productKey1"), "value1")
+ Assert.assertEquals(properties.remove("productKey2"), "value2")
+
+ val emptyAttributes = HashMap()
+ Assert.assertEquals(emptyAttributes, properties)
+ }
+
+ @Test
+ fun testEnhancedPurchase() {
+ val emptyAttributes = HashMap()
+ val kit = MockAppboyKit()
+ val customAttributes = HashMap()
+ customAttributes["key1"] = "value1"
+ customAttributes["key #2"] = "value #3"
+ val transactionAttributes =
+ TransactionAttributes("the id")
+ .setTax(100.0)
+ .setShipping(12.0)
+ .setRevenue(99.0)
+ .setCouponCode("coupon code")
+ .setAffiliation("the affiliation")
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .quantity(5.0)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.PURCHASE, product)
+ .currency("Moon Dollars")
+ .productListName("product list name")
+ .productListSource("the source")
+ .customAttributes(customAttributes)
+ .transactionAttributes(transactionAttributes)
+ .build()
+ kit.logOrderLevelTransaction(commerceEvent)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("Moon Dollars", purchase.currency)
+ Assert.assertEquals(1.0, purchase.quantity.toDouble(), 0.01)
+ Assert.assertEquals("eCommerce - purchase", purchase.sku)
+ Assert.assertEquals(BigDecimal(99.0), purchase.unitPrice)
+ Assert.assertNotNull(purchase.purchaseProperties)
+ val properties = purchase.purchaseProperties.properties
+ val productArray = properties.remove(AppboyKit.PRODUCT_KEY)
+ Assert.assertTrue(productArray is JSONArray)
+ if (productArray is Array<*>) {
+ Assert.assertEquals(1, productArray.size.toLong())
+ val productBrazeProperties = productArray[0]
+ if (productBrazeProperties is BrazeProperties) {
+ val productProperties = productBrazeProperties.properties
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_TOTAL_AMOUNT),
+ 22.5,
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_PRICE),
+ 4.5,
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_QUANTITY),
+ 5.0,
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_ID),
+ "sku1",
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_NAME),
+ "product name",
+ )
+ Assert.assertEquals(emptyAttributes, productProperties)
+ }
+ }
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_SHIPPING), 12.0)
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TAX), 100.0)
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_COUPON_CODE),
+ "coupon code",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_AFFILIATION),
+ "the affiliation",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_ACTION_PRODUCT_LIST_SOURCE),
+ "the source",
+ )
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TOTAL), 99.0)
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_ACTION_PRODUCT_ACTION_LIST),
+ "product list name",
+ )
+ Assert.assertEquals(
+ properties.remove(CommerceEventUtils.Constants.ATT_TRANSACTION_ID),
+ "the id",
+ )
+
+ val brazeCustomAttributesDictionary = properties.remove(AppboyKit.CUSTOM_ATTRIBUTES_KEY)
+ if (brazeCustomAttributesDictionary is BrazeProperties) {
+ val customAttributesDictionary = brazeCustomAttributesDictionary.properties
+ Assert.assertEquals(customAttributesDictionary.remove("key1"), "value1")
+ Assert.assertEquals(customAttributesDictionary.remove("key #2"), "value #3")
+ Assert.assertEquals(emptyAttributes, customAttributesDictionary)
+ }
+ Assert.assertEquals(emptyAttributes, properties)
+ }
+
+// @Test
+// fun testPromotion() {
+// val emptyAttributes = HashMap()
+// val kit = MockAppboyKit()
+// kit.configuration = MockKitConfiguration()
+// val customAttributes = HashMap()
+// customAttributes["key1"] = "value1"
+// customAttributes["key #2"] = "value #3"
+// val promotion = Promotion().apply {
+// id = "my_promo_1"
+// creative = "sale_banner_1"
+// name = "App-wide 50% off sale"
+// position ="dashboard_bottom"
+// }
+// val commerceEvent = CommerceEvent.Builder(Promotion.VIEW, promotion)
+// .customAttributes(customAttributes)
+// .build()
+// kit.logEvent(commerceEvent)
+//
+// val braze = Braze
+// val events = braze.events
+// Assert.assertEquals(1, events.size.toLong())
+// val event = events.values.iterator().next()
+// Assert.assertNotNull(event.properties)
+// val properties = event.properties
+//
+// Assert.assertEquals(properties.remove("Id"), "my_promo_1")
+// Assert.assertEquals(properties.remove("Name"), "App-wide 50% off sale")
+// Assert.assertEquals(properties.remove("Position"), "dashboard_bottom")
+// Assert.assertEquals(properties.remove("Creative"), "sale_banner_1")
+// Assert.assertEquals(properties.remove("key1"), "value1")
+// Assert.assertEquals(properties.remove("key #2"), "value #3")
+//
+// Assert.assertEquals(emptyAttributes, properties)
+// }
+
+ @Test
+ fun testEnhancedPromotion() {
+ val emptyAttributes = HashMap()
+ val kit = MockAppboyKit()
+ kit.configuration = MockKitConfiguration()
+ val customAttributes = HashMap()
+ customAttributes["key1"] = "value1"
+ customAttributes["key #2"] = "value #3"
+ val promotion =
+ Promotion().apply {
+ id = "my_promo_1"
+ creative = "sale_banner_1"
+ name = "App-wide 50% off sale"
+ position = "dashboard_bottom"
+ }
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Promotion.VIEW, promotion)
+ .customAttributes(customAttributes)
+ .build()
+ kit.logOrderLevelTransaction(commerceEvent)
+ val braze = Braze
+ val events = braze.events
+ Assert.assertEquals(1, events.size.toLong())
+ val event = events.values.iterator().next()
+ Assert.assertNotNull(event.properties)
+ val properties = event.properties
+
+ val promotionArray = properties.remove(AppboyKit.PROMOTION_KEY)
+ Assert.assertTrue(promotionArray is JSONArray)
+ if (promotionArray is Array<*>) {
+ Assert.assertEquals(1, promotionArray.size.toLong())
+ val promotionBrazeProperties = promotionArray[0]
+ if (promotionBrazeProperties is BrazeProperties) {
+ val promotionProperties = promotionBrazeProperties.properties
+ Assert.assertEquals(
+ promotionProperties.remove(CommerceEventUtils.Constants.ATT_PROMOTION_ID),
+ "my_promo_1",
+ )
+ Assert.assertEquals(
+ promotionProperties.remove(CommerceEventUtils.Constants.ATT_PROMOTION_NAME),
+ "App-wide 50% off sale",
+ )
+ Assert.assertEquals(
+ promotionProperties.remove(CommerceEventUtils.Constants.ATT_PROMOTION_POSITION),
+ "dashboard_bottom",
+ )
+ Assert.assertEquals(
+ promotionProperties.remove(CommerceEventUtils.Constants.ATT_PROMOTION_CREATIVE),
+ "sale_banner_1",
+ )
+ Assert.assertEquals(emptyAttributes, promotionProperties)
+ }
+ }
+
+ val brazeCustomAttributesDictionary = properties.remove(AppboyKit.CUSTOM_ATTRIBUTES_KEY)
+ if (brazeCustomAttributesDictionary is BrazeProperties) {
+ val customAttributesDictionary = brazeCustomAttributesDictionary.properties
+ Assert.assertEquals(customAttributesDictionary.remove("key1"), "value1")
+ Assert.assertEquals(customAttributesDictionary.remove("key #2"), "value #3")
+ Assert.assertEquals(emptyAttributes, customAttributesDictionary)
+ }
+
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TOTAL), 0.0)
+
+ Assert.assertEquals(emptyAttributes, properties)
+ }
+
+// @Test
+// fun testImpression() {
+// val kit = MockAppboyKit()
+// kit.configuration = MockKitConfiguration()
+// val customAttributes = HashMap()
+// customAttributes["key1"] = "value1"
+// customAttributes["key #2"] = "value #3"
+// val product = Product.Builder("product name", "sku1", 4.5)
+// .quantity(5.0)
+// .build()
+// val impression = Impression("Suggested Products List", product).let {
+// CommerceEvent.Builder(it).build()
+// }
+// val commerceEvent = CommerceEvent.Builder(impression)
+// .customAttributes(customAttributes)
+// .build()
+//
+// kit.logEvent(commerceEvent)
+//
+// val braze = Braze
+// val events = braze.events
+// Assert.assertEquals(1, events.size.toLong())
+// val event = events.values.iterator().next()
+// Assert.assertNotNull(event.properties)
+// val properties = event.properties
+//
+// Assert.assertEquals(
+// properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_TOTAL_AMOUNT), "22.5"
+// )
+// Assert.assertEquals(
+// properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_NAME), "product name"
+// )
+// Assert.assertEquals(
+// properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_QUANTITY), "5.0"
+// )
+// Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_ID), "sku1")
+// Assert.assertEquals(
+// properties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_PRICE), "4.5"
+// )
+// Assert.assertEquals(
+// properties.remove("Product Impression List"), "Suggested Products List"
+// )
+// Assert.assertEquals(properties.remove("key1"), "value1")
+// Assert.assertEquals(properties.remove("key #2"), "value #3")
+//
+// val emptyAttributes = HashMap()
+// Assert.assertEquals(emptyAttributes, properties)
+// }
+
+ @Test
+ fun testEnhancedImpression() {
+ val emptyAttributes = HashMap()
+ val kit = MockAppboyKit()
+ kit.configuration = MockKitConfiguration()
+ val customAttributes = HashMap()
+ customAttributes["key1"] = "value1"
+ customAttributes["key #2"] = "value #3"
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .quantity(5.0)
+ .customAttributes(customAttributes)
+ .build()
+ val impression =
+ Impression("Suggested Products List", product).let {
+ CommerceEvent.Builder(it).build()
+ }
+ val commerceEvent =
+ CommerceEvent
+ .Builder(impression)
+ .customAttributes(customAttributes)
+ .build()
+ kit.logOrderLevelTransaction(commerceEvent)
+ val braze = Braze
+ val events = braze.events
+ Assert.assertEquals(1, events.size.toLong())
+ val event = events.values.iterator().next()
+ Assert.assertNotNull(event.properties)
+ val properties = event.properties
+
+ val impressionArray = properties.remove(AppboyKit.IMPRESSION_KEY)
+ Assert.assertTrue(impressionArray is JSONArray)
+ if (impressionArray is Array<*>) {
+ Assert.assertEquals(1, impressionArray.size.toLong())
+ val impressionBrazeProperties = impressionArray[0]
+ if (impressionBrazeProperties is BrazeProperties) {
+ val impressionProperties = impressionBrazeProperties.properties
+ Assert.assertEquals(
+ impressionProperties.remove("Product Impression List"),
+ "Suggested Products List",
+ )
+ val productArray = impressionProperties.remove(AppboyKit.PRODUCT_KEY)
+ Assert.assertTrue(productArray is Array<*>)
+ if (productArray is Array<*>) {
+ Assert.assertEquals(1, productArray.size.toLong())
+ val productBrazeProperties = productArray[0]
+ if (productBrazeProperties is BrazeProperties) {
+ val productProperties = productBrazeProperties.properties
+ Assert.assertEquals(
+ productProperties.remove(
+ CommerceEventUtils.Constants.ATT_PRODUCT_TOTAL_AMOUNT,
+ ),
+ 22.5,
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_NAME),
+ "product name",
+ )
+ Assert.assertEquals(
+ productProperties.remove(
+ CommerceEventUtils.Constants.ATT_PRODUCT_QUANTITY,
+ ),
+ 5.0,
+ )
+ Assert.assertEquals(
+ productProperties.remove(CommerceEventUtils.Constants.ATT_PRODUCT_ID),
+ "sku1",
+ )
+ Assert.assertEquals(
+ productProperties.remove(
+ CommerceEventUtils.Constants.ATT_PRODUCT_PRICE,
+ ),
+ 4.5,
+ )
+ val brazeProductCustomAttributesDictionary =
+ productProperties.remove(AppboyKit.CUSTOM_ATTRIBUTES_KEY)
+ if (brazeProductCustomAttributesDictionary is BrazeProperties) {
+ val customProductAttributesDictionary =
+ brazeProductCustomAttributesDictionary.properties
+ Assert.assertEquals(
+ customProductAttributesDictionary.remove("key1"),
+ "value1",
+ )
+ Assert.assertEquals(
+ customProductAttributesDictionary.remove("key #2"),
+ "value #3",
+ )
+ Assert.assertEquals(emptyAttributes, customProductAttributesDictionary)
+ }
+ Assert.assertEquals(emptyAttributes, productProperties)
+ }
+ Assert.assertEquals(emptyAttributes, impressionProperties)
+ }
+ }
+ }
+
+ val brazeCustomAttributesDictionary = properties.remove(AppboyKit.CUSTOM_ATTRIBUTES_KEY)
+ if (brazeCustomAttributesDictionary is BrazeProperties) {
+ val customAttributesDictionary = brazeCustomAttributesDictionary.properties
+ Assert.assertEquals(customAttributesDictionary.remove("key1"), "value1")
+ Assert.assertEquals(customAttributesDictionary.remove("key #2"), "value #3")
+ Assert.assertEquals(emptyAttributes, customAttributesDictionary)
+ }
+
+ Assert.assertEquals(properties.remove(CommerceEventUtils.Constants.ATT_TOTAL), 0.0)
+
+ Assert.assertEquals(emptyAttributes, properties)
+ }
+
+// @Test
+// fun setUserAttributeTyped() {
+// val kit = MockAppboyKit()
+// kit.enableTypeDetection = true
+// val currentUser = Braze.currentUser
+// kit.setUserAttribute("foo", "true")
+// Assert.assertTrue(currentUser.customUserAttributes["foo"] is Boolean)
+// Assert.assertEquals(currentUser.customUserAttributes["foo"], true)
+// kit.setUserAttribute("foo", "1")
+// Assert.assertTrue(currentUser.customUserAttributes["foo"] is Int)
+// Assert.assertEquals(currentUser.customUserAttributes["foo"], 1)
+// kit.setUserAttribute("foo", "1.1")
+// Assert.assertTrue(currentUser.customUserAttributes["foo"] is Double)
+// Assert.assertEquals(currentUser.customUserAttributes["foo"], 1.1)
+// kit.setUserAttribute("foo", "bar")
+// Assert.assertTrue(currentUser.customUserAttributes["foo"] is String)
+// Assert.assertEquals(currentUser.customUserAttributes["foo"], "bar")
+// }
+
+// @Test
+// fun testEventStringType() {
+// val kit = MockAppboyKit()
+// kit.configuration = MockKitConfiguration()
+// val customAttributes = HashMap()
+// customAttributes["foo"] = "false"
+// customAttributes["bar"] = "1"
+// customAttributes["baz"] = "1.5"
+// customAttributes["fuzz?"] = "foobar"
+// val customEvent = MPEvent.Builder("testEvent", MParticle.EventType.Location)
+// .customAttributes(customAttributes)
+// .build()
+// kit.enableTypeDetection = true
+// kit.logEvent(customEvent)
+// val braze = Braze
+// val events = braze.events
+// Assert.assertEquals(1, events.values.size.toLong())
+// val event = events.values.iterator().next()
+// val properties = event.properties
+// Assert.assertEquals(properties.remove("foo"), false)
+// Assert.assertEquals(properties.remove("bar"), 1)
+// Assert.assertEquals(properties.remove("baz"), 1.5)
+// Assert.assertEquals(properties.remove("fuzz?"), "foobar")
+// Assert.assertEquals(0, properties.size.toLong())
+// }
+
+// @Test
+// fun testLogCommerceEvent() {
+// val kit = MockAppboyKit()
+//
+// val product: Product = Product.Builder("La Enchilada", "13061043670", 12.5)
+// .quantity(1.0)
+// .build()
+//
+// val txAttributes = TransactionAttributes()
+// .setRevenue(product.getTotalAmount())
+//
+// kit.configuration = MockKitConfiguration()
+// val customAttributes: MutableMap = HashMap()
+// customAttributes["currentLocationLongitude"] = "2.1811267"
+// customAttributes["country"] = "ES"
+// customAttributes["deliveryLocationLatitude"] = "41.4035798"
+// customAttributes["appVersion"] = "5.201.0"
+// customAttributes["city"] = "BCN"
+// customAttributes["deviceId"] = "1104442582"
+// customAttributes["platform"] = "android"
+// customAttributes["isAuthorized"] = "true"
+// customAttributes["productSelectionOrigin"] = "Catalogue"
+// customAttributes["currentLocationLatitude"] = "41.4035798"
+// customAttributes["collectionId"] = "1180889389"
+// customAttributes["multiplatformVersion"] = "1.0.288"
+// customAttributes["deliveryLocationTimestamp"] = "1675344636685"
+// customAttributes["productId"] = "13061043670"
+// customAttributes["storeAddressId"] = "300482"
+// customAttributes["currentLocationAccuracy"] = "19.278"
+// customAttributes["productAddedOrigin"] = "Item Detail Add to Order"
+// customAttributes["deliveryLocationLongitude"] = "2.1811267"
+// customAttributes["currentLocationTimestamp"] = "1675344636685"
+// customAttributes["dynamicSessionId"] = "67f8fb8d-8d14-4f0e-bf1a-73fb8e6eed95"
+// customAttributes["deliveryLocationAccuracy"] = "19.278"
+// customAttributes["categoryId"] = "1"
+// customAttributes["isSponsored"] = "false"
+//
+// val commerceEvent: CommerceEvent = CommerceEvent.Builder(Product.ADD_TO_CART, product)
+// .currency("EUR")
+// .customAttributes(customAttributes)
+// .transactionAttributes(txAttributes)
+// .build()
+// kit.logEvent(commerceEvent)
+//
+// val braze = Braze
+// val events = braze.events
+// Assert.assertEquals(1, events.size.toLong())
+// val event = events.values.iterator().next()
+// Assert.assertNotNull(event.properties)
+// val properties = event.properties
+//
+// Assert.assertEquals(properties.remove("Name"), "La Enchilada")
+// Assert.assertEquals(properties.remove("Total Product Amount"), "12.5")
+// Assert.assertEquals(properties.remove("Id"), "13061043670")
+// for (item in customAttributes) {
+// Assert.assertTrue(properties.containsKey(item.key))
+// Assert.assertTrue(properties.containsValue(item.value))
+// }
+// }
+
+// @Test
+// fun testEventStringTypeNotEnabled() {
+// val kit = MockAppboyKit()
+// kit.configuration = MockKitConfiguration()
+// val customAttributes = HashMap()
+// customAttributes["foo"] = "false"
+// customAttributes["bar"] = "1"
+// customAttributes["baz"] = "1.5"
+// customAttributes["fuzz?"] = "foobar"
+// val customEvent = MPEvent.Builder("testEvent", MParticle.EventType.Location)
+// .customAttributes(customAttributes)
+// .build()
+// kit.enableTypeDetection = false
+// kit.logEvent(customEvent)
+// val braze = Braze
+// val events = braze.events
+// Assert.assertEquals(1, events.values.size.toLong())
+// val event = events.values.iterator().next()
+// val properties = event.properties
+// Assert.assertEquals(properties.remove("foo"), "false")
+// Assert.assertEquals(properties.remove("bar"), "1")
+// Assert.assertEquals(properties.remove("baz"), "1.5")
+// Assert.assertEquals(properties.remove("fuzz?"), "foobar")
+// Assert.assertEquals(0, properties.size.toLong())
+// }
+
+ @Test
+ fun testCustomAttributes_log_add_attribute_event() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+
+ kit.configuration = MockKitConfiguration()
+
+ val jsonObject = JSONObject()
+ val mapValue = JSONObject()
+ // this is hash for event attribute i.e combination of eventType + eventName + attribute key
+ mapValue.put("888169310", "testEvent")
+ val eaaObject = JSONObject()
+ eaaObject.put("eaa", mapValue)
+ jsonObject.put("hs", eaaObject)
+
+ Mockito.`when`(mTypeFilters!!.size()).thenReturn(0)
+
+ var kitConfiguration = MockKitConfiguration.createKitConfiguration(jsonObject)
+ kit.configuration = kitConfiguration
+ val customAttributes: MutableMap = HashMap()
+ customAttributes["destination"] = "Shop"
+ val event =
+ MPEvent
+ .Builder("AndroidTEST", MParticle.EventType.Navigation)
+ .customAttributes(customAttributes)
+ .build()
+ val instance = MParticle.getInstance()
+ instance?.logEvent(event)
+ kit.logEvent(event)
+ Assert.assertEquals(1, braze.events.size.toLong())
+ Assert.assertEquals(1, currentUser.getCustomAttribute().size.toLong())
+ var outputKey = ""
+ for (keys in currentUser.getCustomAttribute().keys) {
+ outputKey = keys
+ break
+ }
+ Assert.assertEquals("testEvent", outputKey)
+ }
+
+ @Test
+ fun testCustomAttributes_log_remove_attribute_event() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+
+ kit.configuration = MockKitConfiguration()
+
+ val jsonObject = JSONObject()
+ val mapValue = JSONObject()
+ // this is hash for event attribute i.e combination of eventType + eventName + attribute key
+ mapValue.put("888169310", "testEvent")
+ val eaaObject = JSONObject()
+ eaaObject.put("eaa", mapValue)
+ eaaObject.put("ear", mapValue)
+ jsonObject.put("hs", eaaObject)
+
+ Mockito.`when`(mTypeFilters!!.size()).thenReturn(0)
+
+ var kitConfiguration = MockKitConfiguration.createKitConfiguration(jsonObject)
+ kit.configuration = kitConfiguration
+ val customAttributes: MutableMap = HashMap()
+ customAttributes["destination"] = "Shop"
+ val event =
+ MPEvent
+ .Builder("AndroidTEST", MParticle.EventType.Navigation)
+ .customAttributes(customAttributes)
+ .build()
+ val instance = MParticle.getInstance()
+ instance?.logEvent(event)
+ kit.logEvent(event)
+ Assert.assertEquals(1, braze.events.size.toLong())
+ Assert.assertEquals(0, currentUser.getCustomAttribute().size.toLong())
+ }
+
+ @Test
+ fun testCustomAttributes_log_add_customUserAttribute_event() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+
+ kit.configuration = MockKitConfiguration()
+
+ val jsonObject = JSONObject()
+ val mapValue = JSONObject()
+ // this is hash for event attribute i.e combination of eventType + eventName + attribute key
+ mapValue.put("888169310", "testEvent")
+ val eaaObject = JSONObject()
+ eaaObject.put("eas", mapValue)
+ jsonObject.put("hs", eaaObject)
+
+ Mockito.`when`(mTypeFilters!!.size()).thenReturn(0)
+
+ var kitConfiguration = MockKitConfiguration.createKitConfiguration(jsonObject)
+ kit.configuration = kitConfiguration
+ val customAttributes: MutableMap = HashMap()
+ customAttributes["destination"] = "Shop"
+ val event =
+ MPEvent
+ .Builder("AndroidTEST", MParticle.EventType.Navigation)
+ .customAttributes(customAttributes)
+ .build()
+ val instance = MParticle.getInstance()
+ instance?.logEvent(event)
+ kit.logEvent(event)
+ Assert.assertEquals(1, braze.events.size.toLong())
+ Assert.assertEquals(1, currentUser.getCustomUserAttribute().size.toLong())
+ var outputKey = ""
+ for (keys in currentUser.getCustomUserAttribute().keys) {
+ outputKey = keys
+ break
+ }
+ Assert.assertEquals("testEvent", outputKey)
+ }
+
+ @Test
+ fun testParseToNestedMap_When_JSON_Is_INVALID() {
+ val kit = MockAppboyKit()
+ var jsonInput =
+ "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent'," +
+ "'location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}," +
+ "'performance':'{'consented':true,'timestamp':1711038269644," +
+ "'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3'," +
+ "'hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}," +
+ "'CCPA':'{'consented':true,'timestamp':1711038269644," +
+ "'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane'," +
+ "'hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}"
+
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "parseToNestedMap",
+ String::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, jsonInput)
+ Assert.assertEquals(mutableMapOf(), result)
+ }
+
+ @Test
+ fun testParseToNestedMap_When_JSON_Is_Empty() {
+ val kit = MockAppboyKit()
+ var jsonInput = ""
+
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "parseToNestedMap",
+ String::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, jsonInput)
+ Assert.assertEquals(mutableMapOf(), result)
+ }
+
+ @Test
+ fun testSearchKeyInNestedMap_When_Input_Key_Is_Empty_String() {
+ val kit = MockAppboyKit()
+ val map =
+ mapOf(
+ "FeatureEnabled" to true,
+ "settings" to
+ mapOf(
+ "darkMode" to false,
+ "notifications" to
+ mapOf(
+ "email" to false,
+ "push" to true,
+ "lastUpdated" to 1633046400000L,
+ ),
+ ),
+ )
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "searchKeyInNestedMap",
+ Map::class.java,
+ Any::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, map, "")
+ Assert.assertEquals(null, result)
+ }
+
+ @Test
+ fun testSearchKeyInNestedMap_When_Input_Is_Empty_Map() {
+ val kit = MockAppboyKit()
+ val emptyMap: Map = emptyMap()
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "searchKeyInNestedMap",
+ Map::class.java,
+ Any::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, emptyMap, "1")
+ Assert.assertEquals(null, result)
+ }
+
+ @Test
+ fun testParseConsentMapping_When_Input_Is_Empty_Json() {
+ val kit = MockAppboyKit()
+ val emptyJson = ""
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "parseConsentMapping",
+ String::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, emptyJson)
+ Assert.assertEquals(emptyMap(), result)
+ }
+
+ @Test
+ fun testParseConsentMapping_When_Input_Is_Invalid_Json() {
+ val kit = MockAppboyKit()
+ var jsonInput =
+ "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent'," +
+ "'location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}," +
+ "'performance':'{'consented':true,'timestamp':1711038269644," +
+ "'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3'," +
+ "'hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}," +
+ "'CCPA':'{'consented':true,'timestamp':1711038269644," +
+ "'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane'," +
+ "'hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}"
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "parseConsentMapping",
+ String::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, jsonInput)
+ Assert.assertEquals(emptyMap(), result)
+ }
+
+ @Test
+ fun testParseConsentMapping_When_Input_Is_NULL() {
+ val kit = MockAppboyKit()
+ val method: Method =
+ AppboyKit::class.java.getDeclaredMethod(
+ "parseConsentMapping",
+ String::class.java,
+ )
+ method.isAccessible = true
+ val result = method.invoke(kit, null)
+ Assert.assertEquals(emptyMap(), result)
+ }
+
+ @Test
+ fun onConsentStateUpdatedTest() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+ kit.configuration = MockKitConfiguration()
+ val map = java.util.HashMap()
+
+ map["consentMappingSDK"] =
+ " [{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\"," +
+ "\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"google_ad_user_data\\\"}," +
+ "{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\"," +
+ "\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"google_ad_personalization\\\"}]"
+
+ var kitConfiguration =
+ MockKitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap()))
+ kit.configuration = kitConfiguration
+
+ val marketingConsent =
+ GDPRConsent
+ .builder(false)
+ .document("Test consent")
+ .location("17 Cherry Tree Lane")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+ val state =
+ ConsentState
+ .builder()
+ .addGDPRConsentState("Marketing", marketingConsent)
+ .build()
+ filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit)
+
+ kit.onConsentStateUpdated(state, state, filteredMParticleUser)
+ TestCase.assertEquals(
+ false,
+ currentUser.getCustomUserAttribute()["\$google_ad_personalization"],
+ )
+ }
+
+ @Test
+ fun onConsentStateUpdatedTest_When_Both_The_consents_Are_True() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+ kit.configuration = MockKitConfiguration()
+ val map = java.util.HashMap()
+
+ map["consentMappingSDK"] =
+ " [{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\"," +
+ "\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"google_ad_user_data\\\"}," +
+ "{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\"," +
+ "\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"google_ad_personalization\\\"}]"
+
+ var kitConfiguration =
+ MockKitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap()))
+ kit.configuration = kitConfiguration
+
+ val marketingConsent =
+ GDPRConsent
+ .builder(true)
+ .document("Test consent")
+ .location("17 Cherry Tree Lane")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val performanceConsent =
+ GDPRConsent
+ .builder(true)
+ .document("parental_consent_agreement_v2")
+ .location("17 Cherry Tree Lan 3")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val state =
+ ConsentState
+ .builder()
+ .addGDPRConsentState("Marketing", marketingConsent)
+ .addGDPRConsentState("Performance", performanceConsent)
+ .build()
+ filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit)
+
+ kit.onConsentStateUpdated(state, state, filteredMParticleUser)
+ TestCase.assertEquals(true, currentUser.getCustomUserAttribute()["\$google_ad_user_data"])
+ TestCase.assertEquals(
+ true,
+ currentUser.getCustomUserAttribute()["\$google_ad_personalization"],
+ )
+ }
+
+ @Test
+ fun onConsentStateUpdatedTest_When_No_DATA_From_Server() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+ kit.configuration = MockKitConfiguration()
+ val marketingConsent =
+ GDPRConsent
+ .builder(true)
+ .document("Test consent")
+ .location("17 Cherry Tree Lane")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val performanceConsent =
+ GDPRConsent
+ .builder(true)
+ .document("parental_consent_agreement_v2")
+ .location("17 Cherry Tree Lan 3")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val state =
+ ConsentState
+ .builder()
+ .addGDPRConsentState("Marketing", marketingConsent)
+ .addGDPRConsentState("Performance", performanceConsent)
+ .build()
+ filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit)
+ kit.onConsentStateUpdated(state, state, filteredMParticleUser)
+ TestCase.assertEquals(0, currentUser.getCustomUserAttribute().size)
+ }
+
+ @Test
+ fun testOnConsentStateUpdatedTest_No_consentMappingSDK() {
+ val kit = MockAppboyKit()
+ val currentUser = braze.currentUser
+ kit.configuration = MockKitConfiguration()
+ val map = java.util.HashMap()
+ map["includeEnrichedUserAttributes"] = "True"
+ map["userIdentificationType"] = "MPID"
+ map["ABKDisableAutomaticLocationCollectionKey"] = "False"
+ map["defaultAdPersonalizationConsentSDK"] = "Denied"
+
+ kit.configuration =
+ KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap()))
+
+ val marketingConsent =
+ GDPRConsent
+ .builder(true)
+ .document("Test consent")
+ .location("17 Cherry Tree Lane")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val performanceConsent =
+ GDPRConsent
+ .builder(true)
+ .document("parental_consent_agreement_v2")
+ .location("17 Cherry Tree Lan 3")
+ .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702")
+ .build()
+
+ val state =
+ ConsentState
+ .builder()
+ .addGDPRConsentState("Marketing", marketingConsent)
+ .addGDPRConsentState("Performance", performanceConsent)
+ .build()
+ filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit)
+
+ kit.onConsentStateUpdated(state, state, filteredMParticleUser)
+
+ TestCase.assertEquals(0, currentUser.getCustomUserAttribute().size)
+ }
+
+ @Test
+ fun testPurchase_Forward_product_name() {
+ var settings = HashMap()
+ settings[AppboyKit.APPBOY_KEY] = "key"
+ settings[AppboyKit.REPLACE_SKU_AS_PRODUCT_NAME] = "True"
+ val kit = MockAppboyKit()
+
+ kit.configuration =
+ KitConfiguration.createKitConfiguration(JSONObject().put("as", settings))
+ kit.onKitCreate(settings, MockContextApplication())
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.CHECKOUT, product)
+ .currency("Moon Dollars")
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("product name", purchase.sku)
+ Assert.assertNull(
+ purchase.purchaseProperties.properties[CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE],
+ )
+ }
+
+ @Test
+ fun testPurchase_Forward_product_name_When_flag_IS_FALSE() {
+ var settings = HashMap()
+ settings[AppboyKit.APPBOY_KEY] = "key"
+ settings[AppboyKit.REPLACE_SKU_AS_PRODUCT_NAME] = "False"
+ val kit = MockAppboyKit()
+
+ kit.configuration =
+ KitConfiguration.createKitConfiguration(JSONObject().put("as", settings))
+ kit.onKitCreate(settings, MockContextApplication())
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.CHECKOUT, product)
+ .currency("Moon Dollars")
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("sku1", purchase.sku)
+ Assert.assertNull(
+ purchase.purchaseProperties.properties[CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE],
+ )
+ }
+
+ @Test
+ fun testPurchase_Forward_product_name_When_flag_IS_Null() {
+ var settings = HashMap()
+ settings[AppboyKit.APPBOY_KEY] = "key"
+ val kit = MockAppboyKit()
+
+ kit.configuration =
+ KitConfiguration.createKitConfiguration(JSONObject().put("as", settings))
+ kit.onKitCreate(settings, MockContextApplication())
+ val product =
+ Product
+ .Builder("product name", "sku1", 4.5)
+ .build()
+ val commerceEvent =
+ CommerceEvent
+ .Builder(Product.CHECKOUT, product)
+ .currency("Moon Dollars")
+ .build()
+ kit.logTransaction(commerceEvent, product)
+ val braze = Braze
+ val purchases = braze.purchases
+ Assert.assertEquals(1, purchases.size.toLong())
+ val purchase = purchases[0]
+ Assert.assertEquals("sku1", purchase.sku)
+ Assert.assertNull(
+ purchase.purchaseProperties.properties[CommerceEventUtils.Constants.ATT_ACTION_CURRENCY_CODE],
+ )
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/BrazePurchase.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/BrazePurchase.kt
new file mode 100644
index 000000000..93de4c921
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/BrazePurchase.kt
@@ -0,0 +1,12 @@
+package com.mparticle.kits
+
+import com.braze.models.outgoing.BrazeProperties
+import java.math.BigDecimal
+
+data class BrazePurchase(
+ val sku: String,
+ val currency: String,
+ val unitPrice: BigDecimal,
+ val quantity: Int,
+ val purchaseProperties: BrazeProperties,
+)
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/TypeDetectionTests.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/TypeDetectionTests.kt
new file mode 100644
index 000000000..c9a3ab91b
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/TypeDetectionTests.kt
@@ -0,0 +1,91 @@
+package com.mparticle.kits
+
+import com.mparticle.kits.AppboyKit.StringTypeParser
+import org.junit.Assert
+import org.junit.Test
+
+class TypeDetectionTests {
+ @Test
+ fun testEnableTypeDetection() {
+ val parser = SomeParser(true)
+ Assert.assertEquals("foo", parser.parseValue("key", "foo"))
+ Assert.assertEquals(1, parser.parseValue("key", "1"))
+ Assert.assertEquals(-99, parser.parseValue("key", "-99"))
+ Assert.assertEquals(Int.MAX_VALUE, parser.parseValue("key", Int.MAX_VALUE.toString()))
+ Assert.assertEquals(Int.MIN_VALUE, parser.parseValue("key", Int.MIN_VALUE.toString()))
+ Assert.assertEquals(
+ Int.MAX_VALUE + 1L,
+ parser.parseValue("key", (Int.MAX_VALUE + 1L).toString()),
+ )
+ Assert.assertEquals(
+ Int.MIN_VALUE - 1L,
+ parser.parseValue("key", (Int.MIN_VALUE - 1L).toString()),
+ )
+ Assert.assertEquals(Long.MAX_VALUE, parser.parseValue("key", Long.MAX_VALUE.toString()))
+ Assert.assertEquals(Long.MIN_VALUE, parser.parseValue("key", Long.MIN_VALUE.toString()))
+ Assert.assertEquals(432.2561, parser.parseValue("key", "432.2561"))
+ Assert.assertEquals(-1.0001, parser.parseValue("key", "-1.0001"))
+ Assert.assertEquals(false, parser.parseValue("key", "false"))
+ Assert.assertEquals(true, parser.parseValue("key", "True"))
+ Assert.assertTrue(parser.parseValue("key", "1.0") is Double)
+ Assert.assertTrue(parser.parseValue("key", (Int.MAX_VALUE + 1L).toString()) is Long)
+ Assert.assertTrue(parser.parseValue("key", Int.MAX_VALUE.toString()) is Int)
+ Assert.assertTrue(parser.parseValue("key", "0") is Int)
+ Assert.assertTrue(parser.parseValue("key", "true") is Boolean)
+ Assert.assertTrue(parser.parseValue("key", "True") is Boolean)
+ }
+
+ @Test
+ fun testDisableTypeDetection() {
+ val parser = SomeParser(false)
+ Assert.assertEquals("foo", parser.parseValue("key", "foo"))
+ Assert.assertEquals("1", parser.parseValue("key", "1"))
+ Assert.assertEquals(
+ (Int.MAX_VALUE + 1L).toString(),
+ parser.parseValue("key", (Int.MAX_VALUE + 1L).toString()),
+ )
+ Assert.assertEquals("432.2561", parser.parseValue("key", "432.2561"))
+ Assert.assertEquals("true", parser.parseValue("key", "true"))
+ }
+
+ private inner class SomeParser internal constructor(
+ enableTypeDetection: Boolean?,
+ ) : StringTypeParser(
+ enableTypeDetection!!,
+ ) {
+ override fun toString(
+ key: String,
+ value: String,
+ ) {
+ // do nothing
+ }
+
+ override fun toInt(
+ key: String,
+ value: Int,
+ ) {
+ // do nothing
+ }
+
+ override fun toLong(
+ key: String,
+ value: Long,
+ ) {
+ // do nothing
+ }
+
+ override fun toDouble(
+ key: String,
+ value: Double,
+ ) {
+ // do nothing
+ }
+
+ override fun toBoolean(
+ key: String,
+ value: Boolean,
+ ) {
+ // do nothing
+ }
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockAppboyKit.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockAppboyKit.kt
new file mode 100644
index 000000000..f96faa2c2
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockAppboyKit.kt
@@ -0,0 +1,29 @@
+package com.mparticle.kits.mocks
+
+import android.content.Context
+import com.mparticle.internal.ReportingManager
+import com.mparticle.kits.AppboyKit
+import org.mockito.Mockito
+
+class MockAppboyKit : AppboyKit() {
+ val calledAuthority = arrayOfNulls(1)
+
+ override fun setAuthority(authority: String?) {
+ calledAuthority[0] = authority
+ }
+
+ override fun queueDataFlush() {
+ // do nothing
+ }
+
+ init {
+ kitManager =
+ MockKitManagerImpl(
+ Mockito.mock(Context::class.java),
+ Mockito.mock(
+ ReportingManager::class.java,
+ ),
+ MockCoreCallbacks(),
+ )
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockApplication.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockApplication.kt
new file mode 100644
index 000000000..be9dc8b59
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockApplication.kt
@@ -0,0 +1,39 @@
+package com.mparticle.kits.mocks
+
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.res.Resources
+
+class MockApplication(
+ var mContext: MockContext,
+) : Application() {
+ var mCallbacks: ActivityLifecycleCallbacks? = null
+
+ override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks) {
+ mCallbacks = callback
+ }
+
+ override fun getApplicationContext(): Context = this
+
+ fun setSharedPreferences(prefs: SharedPreferences) {
+ mContext.setSharedPreferences(prefs)
+ }
+
+ override fun getSystemService(name: String): Any? = mContext.getSystemService(name)
+
+ override fun getSharedPreferences(
+ name: String,
+ mode: Int,
+ ): SharedPreferences = mContext.getSharedPreferences(name, mode)
+
+ override fun getPackageManager(): PackageManager = mContext.packageManager
+
+ override fun getPackageName(): String = mContext.packageName
+
+ override fun getApplicationInfo(): ApplicationInfo = mContext.applicationInfo
+
+ override fun getResources(): Resources? = mContext.resources
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContext.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContext.kt
new file mode 100644
index 000000000..1d620778e
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContext.kt
@@ -0,0 +1,500 @@
+package com.mparticle.kits.mocks
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.content.IntentSender.SendIntentException
+import android.content.ServiceConnection
+import android.content.SharedPreferences
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.AssetManager
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import android.database.DatabaseErrorHandler
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteDatabase.CursorFactory
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.UserHandle
+import android.telephony.TelephonyManager
+import android.view.Display
+import junit.framework.Assert
+import org.mockito.Mockito
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+
+class MockContext : Context() {
+ private var sharedPreferences: SharedPreferences = MockSharedPreferences()
+ private var resources: Resources = MockResources()
+ var application: MockApplication? = null
+
+ fun setSharedPreferences(prefs: SharedPreferences) {
+ sharedPreferences = prefs
+ }
+
+ override fun getApplicationContext(): Context {
+ if (application == null) {
+ application = MockApplication(this)
+ }
+ return application as MockApplication
+ }
+
+ override fun checkCallingOrSelfPermission(permission: String): Int = PackageManager.PERMISSION_GRANTED
+
+ override fun getSharedPreferences(
+ name: String,
+ mode: Int,
+ ): SharedPreferences = sharedPreferences
+
+ override fun getResources(): Resources? = resources
+
+ override fun getSystemService(name: String): Any? =
+ if (name == TELEPHONY_SERVICE) {
+ Mockito.mock(TelephonyManager::class.java)
+ } else {
+ null
+ }
+
+ override fun getPackageManager(): PackageManager {
+ val manager = Mockito.mock(PackageManager::class.java)
+ val info = Mockito.mock(PackageInfo::class.java)
+ info.versionName = "42"
+ info.versionCode = 42
+ val appInfo = Mockito.mock(ApplicationInfo::class.java)
+ try {
+ Mockito
+ .`when`(manager.getPackageInfo(Mockito.anyString(), Mockito.anyInt()))
+ .thenReturn(info)
+ Mockito
+ .`when`(manager.getInstallerPackageName(Mockito.anyString()))
+ .thenReturn("com.mparticle.test.installer")
+ Mockito
+ .`when`(manager.getApplicationInfo(Mockito.anyString(), Mockito.anyInt()))
+ .thenReturn(appInfo)
+ Mockito.`when`(manager.getApplicationLabel(appInfo)).thenReturn("test label")
+ } catch (e: Exception) {
+ Assert.fail(e.toString())
+ }
+ return manager
+ }
+
+ override fun getPackageName(): String = "com.mparticle.test"
+
+ override fun getApplicationInfo(): ApplicationInfo = ApplicationInfo()
+
+ /**
+ * Stubbed methods
+ */
+ override fun setTheme(resid: Int) {}
+
+ override fun getTheme(): Theme? = null
+
+ override fun getClassLoader(): ClassLoader? = null
+
+ override fun sendBroadcast(intent: Intent) {}
+
+ override fun sendBroadcast(
+ intent: Intent,
+ receiverPermission: String?,
+ ) {}
+
+ override fun sendOrderedBroadcast(
+ intent: Intent,
+ receiverPermission: String?,
+ ) {}
+
+ override fun sendOrderedBroadcast(
+ intent: Intent,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) {
+ }
+
+ override fun sendBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ ) {}
+
+ override fun sendBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ receiverPermission: String?,
+ ) {
+ }
+
+ override fun sendOrderedBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) {
+ }
+
+ override fun sendStickyBroadcast(intent: Intent) {}
+
+ override fun sendStickyOrderedBroadcast(
+ intent: Intent,
+ resultReceiver: BroadcastReceiver,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) {
+ }
+
+ override fun removeStickyBroadcast(intent: Intent) {}
+
+ override fun sendStickyBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ ) {}
+
+ override fun sendStickyOrderedBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ resultReceiver: BroadcastReceiver,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?,
+ ) {
+ }
+
+ override fun removeStickyBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ ) {}
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter,
+ ): Intent? = null
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter,
+ flags: Int,
+ ): Intent? = null
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver,
+ filter: IntentFilter,
+ broadcastPermission: String?,
+ scheduler: Handler?,
+ ): Intent? = null
+
+ override fun registerReceiver(
+ receiver: BroadcastReceiver,
+ filter: IntentFilter,
+ broadcastPermission: String?,
+ scheduler: Handler?,
+ flags: Int,
+ ): Intent? = null
+
+ override fun unregisterReceiver(receiver: BroadcastReceiver) {}
+
+ override fun startService(service: Intent): ComponentName? = null
+
+ override fun startForegroundService(service: Intent): ComponentName? = null
+
+ override fun stopService(service: Intent): Boolean = false
+
+ override fun bindService(
+ service: Intent,
+ conn: ServiceConnection,
+ flags: Int,
+ ): Boolean = false
+
+ override fun unbindService(conn: ServiceConnection) {}
+
+ override fun startInstrumentation(
+ className: ComponentName,
+ profileFile: String?,
+ arguments: Bundle?,
+ ): Boolean = false
+
+ override fun checkSelfPermission(permission: String): Int = 0
+
+ override fun enforcePermission(
+ permission: String,
+ pid: Int,
+ uid: Int,
+ message: String?,
+ ) {}
+
+ override fun enforceCallingPermission(
+ permission: String,
+ message: String?,
+ ) {}
+
+ override fun enforceCallingOrSelfPermission(
+ permission: String,
+ message: String?,
+ ) {}
+
+ override fun grantUriPermission(
+ toPackage: String,
+ uri: Uri,
+ modeFlags: Int,
+ ) {}
+
+ override fun revokeUriPermission(
+ uri: Uri,
+ modeFlags: Int,
+ ) {}
+
+ override fun revokeUriPermission(
+ toPackage: String,
+ uri: Uri,
+ modeFlags: Int,
+ ) {}
+
+ override fun checkUriPermission(
+ uri: Uri,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ ): Int = 0
+
+ override fun checkCallingUriPermission(
+ uri: Uri,
+ modeFlags: Int,
+ ): Int = 0
+
+ override fun checkCallingOrSelfUriPermission(
+ uri: Uri,
+ modeFlags: Int,
+ ): Int = 0
+
+ override fun checkUriPermission(
+ uri: Uri?,
+ readPermission: String?,
+ writePermission: String?,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ ): Int = 0
+
+ override fun enforceUriPermission(
+ uri: Uri,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ message: String,
+ ) {
+ }
+
+ override fun enforceCallingUriPermission(
+ uri: Uri,
+ modeFlags: Int,
+ message: String,
+ ) {}
+
+ override fun enforceCallingOrSelfUriPermission(
+ uri: Uri,
+ modeFlags: Int,
+ message: String,
+ ) {}
+
+ override fun enforceUriPermission(
+ uri: Uri?,
+ readPermission: String?,
+ writePermission: String?,
+ pid: Int,
+ uid: Int,
+ modeFlags: Int,
+ message: String?,
+ ) {
+ }
+
+ @Throws(NameNotFoundException::class)
+ override fun createPackageContext(
+ packageName: String,
+ flags: Int,
+ ): Context? = null
+
+ @Throws(NameNotFoundException::class)
+ override fun createContextForSplit(splitName: String): Context? = null
+
+ override fun createConfigurationContext(overrideConfiguration: Configuration): Context? = null
+
+ override fun createDisplayContext(display: Display): Context? = null
+
+ override fun createDeviceProtectedStorageContext(): Context? = null
+
+ override fun isDeviceProtectedStorage(): Boolean = false
+
+ override fun moveSharedPreferencesFrom(
+ sourceContext: Context,
+ name: String,
+ ): Boolean = false
+
+ override fun deleteSharedPreferences(name: String): Boolean = false
+
+ @Throws(FileNotFoundException::class)
+ override fun openFileInput(name: String): FileInputStream? = null
+
+ @Throws(FileNotFoundException::class)
+ override fun openFileOutput(
+ name: String,
+ mode: Int,
+ ): FileOutputStream? = null
+
+ override fun deleteFile(name: String): Boolean = false
+
+ override fun getFileStreamPath(name: String): File? = null
+
+ override fun getDataDir(): File? = null
+
+ override fun getFilesDir(): File? = null
+
+ override fun getNoBackupFilesDir(): File? = null
+
+ override fun getExternalFilesDir(type: String?): File? = null
+
+ override fun getExternalFilesDirs(type: String): Array = arrayOfNulls(0)
+
+ override fun getObbDir(): File? = null
+
+ override fun getObbDirs(): Array = arrayOfNulls(0)
+
+ override fun getCacheDir(): File? = null
+
+ override fun getCodeCacheDir(): File? = null
+
+ override fun getExternalCacheDir(): File? = null
+
+ override fun getExternalCacheDirs(): Array = arrayOfNulls(0)
+
+ override fun getExternalMediaDirs(): Array = arrayOfNulls(0)
+
+ override fun fileList(): Array = arrayOfNulls(0)
+
+ override fun getDir(
+ name: String,
+ mode: Int,
+ ): File? = null
+
+ override fun openOrCreateDatabase(
+ name: String,
+ mode: Int,
+ factory: CursorFactory,
+ ): SQLiteDatabase? = null
+
+ override fun openOrCreateDatabase(
+ name: String,
+ mode: Int,
+ factory: CursorFactory,
+ errorHandler: DatabaseErrorHandler?,
+ ): SQLiteDatabase? = null
+
+ override fun moveDatabaseFrom(
+ sourceContext: Context,
+ name: String,
+ ): Boolean = false
+
+ override fun deleteDatabase(name: String): Boolean = false
+
+ override fun getDatabasePath(name: String): File? = null
+
+ override fun databaseList(): Array = arrayOfNulls(0)
+
+ override fun getWallpaper(): Drawable? = null
+
+ override fun peekWallpaper(): Drawable? = null
+
+ override fun getWallpaperDesiredMinimumWidth(): Int = 0
+
+ override fun getWallpaperDesiredMinimumHeight(): Int = 0
+
+ @Throws(IOException::class)
+ override fun setWallpaper(bitmap: Bitmap) {
+ }
+
+ @Throws(IOException::class)
+ override fun setWallpaper(data: InputStream) {
+ }
+
+ @Throws(IOException::class)
+ override fun clearWallpaper() {
+ }
+
+ override fun startActivity(intent: Intent) {}
+
+ override fun startActivity(
+ intent: Intent,
+ options: Bundle?,
+ ) {}
+
+ override fun startActivities(intents: Array) {}
+
+ override fun startActivities(
+ intents: Array,
+ options: Bundle,
+ ) {}
+
+ @Throws(SendIntentException::class)
+ override fun startIntentSender(
+ intent: IntentSender,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ ) {
+ }
+
+ @Throws(SendIntentException::class)
+ override fun startIntentSender(
+ intent: IntentSender,
+ fillInIntent: Intent?,
+ flagsMask: Int,
+ flagsValues: Int,
+ extraFlags: Int,
+ options: Bundle?,
+ ) {
+ }
+
+ override fun getSystemServiceName(serviceClass: Class<*>): String? = null
+
+ override fun checkPermission(
+ permission: String,
+ pid: Int,
+ uid: Int,
+ ): Int = 0
+
+ override fun checkCallingPermission(permission: String): Int = 0
+
+ override fun getContentResolver(): ContentResolver? = null
+
+ override fun getMainLooper(): Looper? = null
+
+ override fun getPackageResourcePath(): String? = null
+
+ override fun getPackageCodePath(): String? = null
+
+ override fun getAssets(): AssetManager? = null
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContextApplication.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContextApplication.kt
new file mode 100644
index 000000000..274af78e9
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockContextApplication.kt
@@ -0,0 +1,19 @@
+package com.mparticle.kits.mocks
+
+import android.app.Application
+import android.app.Application.ActivityLifecycleCallbacks
+import android.content.Context
+import android.content.SharedPreferences
+
+class MockContextApplication : Application() {
+ override fun getApplicationContext(): Context = this
+
+ override fun getSharedPreferences(
+ name: String,
+ mode: Int,
+ ): SharedPreferences = MockSharedPreferences()
+
+ override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks) {
+ // do nothing
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockCoreCallbacks.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockCoreCallbacks.kt
new file mode 100644
index 000000000..fe538f4d5
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockCoreCallbacks.kt
@@ -0,0 +1,70 @@
+package com.mparticle.kits.mocks
+
+import android.app.Activity
+import android.net.Uri
+import com.mparticle.MParticleOptions.DataplanOptions
+import com.mparticle.internal.CoreCallbacks
+import com.mparticle.internal.CoreCallbacks.KitListener
+import org.json.JSONArray
+import java.lang.ref.WeakReference
+
+class MockCoreCallbacks : CoreCallbacks {
+ override fun isBackgrounded(): Boolean = false
+
+ override fun getUserBucket(): Int = 0
+
+ override fun isEnabled(): Boolean = false
+
+ override fun setIntegrationAttributes(
+ kitId: Int,
+ integrationAttributes: Map,
+ ) {}
+
+ override fun getIntegrationAttributes(kitId: Int): Map? = null
+
+ override fun getCurrentActivity(): WeakReference? = null
+
+ override fun getLatestKitConfiguration(): JSONArray? = null
+
+ override fun getDataplanOptions(): DataplanOptions? = null
+
+ override fun isPushEnabled(): Boolean = false
+
+ override fun getPushSenderId(): String? = null
+
+ override fun getPushInstanceId(): String? = null
+
+ override fun getLaunchUri(): Uri? = null
+
+ override fun getLaunchAction(): String? = null
+
+ override fun getKitListener(): KitListener =
+ object : KitListener {
+ override fun kitFound(kitId: Int) {}
+
+ override fun kitConfigReceived(
+ kitId: Int,
+ configuration: String?,
+ ) {}
+
+ override fun kitExcluded(
+ kitId: Int,
+ reason: String?,
+ ) {}
+
+ override fun kitStarted(kitId: Int) {}
+
+ override fun onKitApiCalled(
+ kitId: Int,
+ used: Boolean?,
+ vararg objects: Any?,
+ ) {}
+
+ override fun onKitApiCalled(
+ methodName: String?,
+ kitId: Int,
+ used: Boolean?,
+ vararg objects: Any?,
+ ) {}
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitConfiguration.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitConfiguration.kt
new file mode 100644
index 000000000..556310a0a
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitConfiguration.kt
@@ -0,0 +1,84 @@
+package com.mparticle.kits.mocks
+
+import android.util.SparseBooleanArray
+import com.mparticle.internal.Logger
+import com.mparticle.kits.KitConfiguration
+import org.json.JSONException
+import org.json.JSONObject
+import java.util.HashMap
+import kotlin.Throws
+
+open class MockKitConfiguration : KitConfiguration() {
+ @Throws(JSONException::class)
+ override fun parseConfiguration(json: JSONObject): KitConfiguration {
+ mTypeFilters = MockSparseBooleanArray()
+ mNameFilters = MockSparseBooleanArray()
+ mAttributeFilters = MockSparseBooleanArray()
+ mScreenNameFilters = MockSparseBooleanArray()
+ mScreenAttributeFilters = MockSparseBooleanArray()
+ mUserIdentityFilters = MockSparseBooleanArray()
+ mUserAttributeFilters = MockSparseBooleanArray()
+ mCommerceAttributeFilters = MockSparseBooleanArray()
+ mCommerceEntityFilters = MockSparseBooleanArray()
+ return super.parseConfiguration(json)
+ }
+
+ override fun convertToSparseArray(json: JSONObject): SparseBooleanArray {
+ val map: SparseBooleanArray = MockSparseBooleanArray()
+ val iterator = json.keys()
+ while (iterator.hasNext()) {
+ try {
+ val key = iterator.next().toString()
+ map.put(key.toInt(), json.getInt(key) == 1)
+ } catch (jse: JSONException) {
+ Logger.error("Issue while parsing kit configuration: " + jse.message)
+ }
+ }
+ return map
+ }
+
+ internal inner class MockSparseBooleanArray : SparseBooleanArray() {
+ override fun get(key: Int): Boolean = get(key, false)
+
+ override fun get(
+ key: Int,
+ valueIfKeyNotFound: Boolean,
+ ): Boolean {
+ print("SparseArray getting: $key")
+ return if (map.containsKey(key)) {
+ true
+ } else {
+ valueIfKeyNotFound
+ }
+ }
+
+ var map: MutableMap = HashMap()
+
+ override fun put(
+ key: Int,
+ value: Boolean,
+ ) {
+ map[key] = value
+ }
+
+ override fun clear() {
+ map.clear()
+ }
+
+ override fun size(): Int = map.size
+
+ override fun toString(): String = map.toString()
+ }
+
+ companion object {
+ @Throws(JSONException::class)
+ fun createKitConfiguration(json: JSONObject): KitConfiguration = MockKitConfiguration().parseConfiguration(json)
+
+ @Throws(JSONException::class)
+ fun createKitConfiguration(): KitConfiguration {
+ val jsonObject = JSONObject()
+ jsonObject.put("id", 42)
+ return MockKitConfiguration().parseConfiguration(jsonObject)
+ }
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitManagerImpl.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitManagerImpl.kt
new file mode 100644
index 000000000..d4a03754b
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockKitManagerImpl.kt
@@ -0,0 +1,71 @@
+package com.mparticle.kits.mocks
+
+import android.content.Context
+import com.mparticle.MParticleOptions
+import com.mparticle.internal.CoreCallbacks
+import com.mparticle.internal.CoreCallbacks.KitListener
+import com.mparticle.internal.ReportingManager
+import com.mparticle.kits.KitConfiguration
+import com.mparticle.kits.KitManagerImpl
+import org.json.JSONException
+import org.json.JSONObject
+import org.mockito.Mockito
+import kotlin.Throws
+
+class MockKitManagerImpl(
+ context: Context?,
+ reportingManager: ReportingManager?,
+ coreCallbacks: CoreCallbacks?,
+) : KitManagerImpl(
+ context,
+ reportingManager,
+ coreCallbacks,
+ Mockito.mock(
+ MParticleOptions::class.java,
+ ),
+) {
+ constructor() : this(
+ MockContext(),
+ Mockito.mock(ReportingManager::class.java),
+ Mockito.mock(
+ CoreCallbacks::class.java,
+ ),
+ ) {
+ Mockito.`when`(mCoreCallbacks.getKitListener()).thenReturn(
+ object : KitListener {
+ override fun kitFound(kitId: Int) {}
+
+ override fun kitConfigReceived(
+ kitId: Int,
+ configuration: String?,
+ ) {}
+
+ override fun kitExcluded(
+ kitId: Int,
+ reason: String?,
+ ) {}
+
+ override fun kitStarted(kitId: Int) {}
+
+ override fun onKitApiCalled(
+ kitId: Int,
+ used: Boolean?,
+ vararg objects: Any?,
+ ) {}
+
+ override fun onKitApiCalled(
+ methodName: String?,
+ kitId: Int,
+ used: Boolean?,
+ vararg objects: Any?,
+ ) {}
+ },
+ )
+ }
+
+ @Throws(JSONException::class)
+ override fun createKitConfiguration(configuration: JSONObject): KitConfiguration =
+ MockKitConfiguration.createKitConfiguration(configuration)
+
+ override fun getUserBucket(): Int = 50
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockResources.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockResources.kt
new file mode 100644
index 000000000..4b2dca6ba
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockResources.kt
@@ -0,0 +1,40 @@
+package com.mparticle.kits.mocks
+
+import android.content.res.Resources
+import android.content.res.Resources.NotFoundException
+import kotlin.Throws
+
+class MockResources : Resources(null, null, null) {
+ override fun getIdentifier(
+ name: String,
+ defType: String,
+ defPackage: String,
+ ): Int {
+ if (name == "mp_key") {
+ return 1
+ } else if (name == "mp_secret") {
+ return 2
+ }
+ return 0
+ }
+
+ @Throws(NotFoundException::class)
+ override fun getString(id: Int): String {
+ when (id) {
+ 1 -> return testAppKey
+ 2 -> return testAppSecret
+ }
+ return ""
+ }
+
+ @Throws(NotFoundException::class)
+ override fun getString(
+ id: Int,
+ vararg formatArgs: Any,
+ ): String = super.getString(id, *formatArgs)
+
+ companion object {
+ var testAppKey = "the app key"
+ var testAppSecret = "the app secret"
+ }
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockSharedPreferences.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockSharedPreferences.kt
new file mode 100644
index 000000000..70f560949
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockSharedPreferences.kt
@@ -0,0 +1,88 @@
+package com.mparticle.kits.mocks
+
+import android.content.SharedPreferences
+import android.content.SharedPreferences.Editor
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import java.util.TreeSet
+
+class MockSharedPreferences :
+ SharedPreferences,
+ Editor {
+ override fun getAll(): Map? = null
+
+ override fun getString(
+ key: String,
+ defValue: String?,
+ ): String = ""
+
+ override fun getStringSet(
+ key: String,
+ defValues: Set?,
+ ): Set = TreeSet()
+
+ override fun getInt(
+ key: String,
+ defValue: Int,
+ ): Int = 0
+
+ override fun getLong(
+ key: String,
+ defValue: Long,
+ ): Long = 0
+
+ override fun getFloat(
+ key: String,
+ defValue: Float,
+ ): Float = 0f
+
+ override fun getBoolean(
+ key: String,
+ defValue: Boolean,
+ ): Boolean = false
+
+ override fun contains(key: String): Boolean = false
+
+ override fun edit(): Editor = this
+
+ override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {}
+
+ override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {}
+
+ override fun putString(
+ key: String,
+ value: String?,
+ ): Editor = this
+
+ override fun putStringSet(
+ key: String,
+ values: Set?,
+ ): Editor = this
+
+ override fun putInt(
+ key: String,
+ value: Int,
+ ): Editor = this
+
+ override fun putLong(
+ key: String,
+ value: Long,
+ ): Editor = this
+
+ override fun putFloat(
+ key: String,
+ value: Float,
+ ): Editor = this
+
+ override fun putBoolean(
+ key: String,
+ value: Boolean,
+ ): Editor = this
+
+ override fun remove(key: String): Editor = this
+
+ override fun clear(): Editor = this
+
+ override fun commit(): Boolean = false
+
+ override fun apply() {}
+}
diff --git a/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockUser.kt b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockUser.kt
new file mode 100644
index 000000000..35125bb28
--- /dev/null
+++ b/kits/braze/braze-39/src/test/kotlin/com/mparticle/kits/mocks/MockUser.kt
@@ -0,0 +1,53 @@
+package com.mparticle.kits.mocks
+
+import com.mparticle.MParticle.IdentityType
+import com.mparticle.UserAttributeListenerType
+import com.mparticle.audience.AudienceResponse
+import com.mparticle.audience.AudienceTask
+import com.mparticle.consent.ConsentState
+import com.mparticle.identity.MParticleUser
+
+class MockUser(
+ var identities: Map,
+) : MParticleUser {
+ override fun getId(): Long = 0
+
+ override fun getUserAttributes(): Map = mapOf()
+
+ override fun getUserAttributes(p0: UserAttributeListenerType?): MutableMap? = null
+
+ override fun setUserAttributes(map: Map): Boolean = false
+
+ override fun getUserIdentities(): Map = identities
+
+ override fun setUserAttribute(
+ s: String,
+ o: Any,
+ ): Boolean = false
+
+ override fun setUserAttributeList(
+ s: String,
+ o: Any,
+ ): Boolean = false
+
+ override fun incrementUserAttribute(
+ p0: String,
+ p1: Number?,
+ ): Boolean = false
+
+ override fun removeUserAttribute(s: String): Boolean = false
+
+ override fun setUserTag(s: String): Boolean = false
+
+ override fun getConsentState(): ConsentState = consentState
+
+ override fun setConsentState(consentState: ConsentState?) {}
+
+ override fun isLoggedIn(): Boolean = false
+
+ override fun getFirstSeenTime(): Long = 0
+
+ override fun getLastSeenTime(): Long = 0
+
+ override fun getUserAudiences(): AudienceTask = throw NotImplementedError("getUserAudiences() is not implemented")
+}
diff --git a/settings-kits.gradle b/settings-kits.gradle
index a12f3c55a..5316a726a 100644
--- a/settings-kits.gradle
+++ b/settings-kits.gradle
@@ -9,6 +9,7 @@ include (
':kits:apptimize:apptimize-3',
//blueshift hosts kit
':kits:braze:braze-38',
+ ':kits:braze:braze-39',
':kits:branch:branch-5',
':kits:clevertap:clevertap-7',
':kits:comscore:comscore-6',