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',