diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..abae093
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,14 @@
+name: Android CI
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ - name: Run CI
+ run: make ci
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c1c5557
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+release/
+
+/.idea/*
+!/.idea/runConfigurations/
+/batch-maven-secret-gpg.key
+/maven.properties
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..6d5b400
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+1.0.0-rc.1
+-----
+
+ * Initial dispatcher release candidate.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..520ed29
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022-Present Batch.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c5b03a8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+all: aar
+
+aar: clean
+ ./gradlew assembleRelease --no-build-cache && \
+ mkdir -p release/ && \
+ cp piano-dispatcher/build/outputs/aar/piano-dispatcher-release.aar release/ && \
+ cp LICENSE release/
+
+clean:
+ ./gradlew clean
+ rm -rf release/
+
+test:
+ ./gradlew testDebugUnitTest
+
+lint:
+ ./gradlew lintDebug
+
+ci: clean lint test aar
+
+publish: aar
+ ./gradlew piano-dispatcher:publish
+
+.PHONY: test aar
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bb595ab
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.batch.android/piano-dispatcher/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.batch.android/piano-dispatcher)
+[![Github Action](https://github.com/BatchLabs/Batch-Android-piano-dispatcher/workflows/Android%20CI/badge.svg)](https://github.com/BatchLabs/Batch-Android-piano-dispatcher/actions?query=workflow%3A%22Android+CI%22)
+
+Batch.com Android Piano Analytics Dispatcher
+==================
+
+![Logo](https://static.batch.com/documentation/Readmes/logo_batch_full_178.png)
+
+# About
+
+A "ready-to-go" dispatcher that reflect event from the Batch SDK to the Piano dashboard using AT and UTM tags.
+
+# Requirements
+ - Android Studio 3.0+
+ - Android 16+
+ - Batch Android SDK 1.19.3+
+ - Piano Analytics 3.1.0+
+
+# Installation
+Gradle (recommended)
+
+```
+implementation 'com.batch.android:piano-dispatcher:1.0.0'
+```
+
+Read our [setup documentation](https://doc.batch.com/) to follow a step by step tutorial for integrating Batch features into your app.
+
+# Documentation
+
+ - [Technical](https://batch.com/doc)
+ - [FAQ](https://batch.com/doc/faq/general.html)
+ - [Developer FAQ](https://batch.com/developers)
+
+Copyright 2020 - Batch.com
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..9c0136a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.library' version '7.3.0' apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3e927b1
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4bec442
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Nov 08 14:28:42 CET 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or 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 UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$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 "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# 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
+ ;;
+ 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/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/piano-dispatcher/.gitignore b/piano-dispatcher/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/piano-dispatcher/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/piano-dispatcher/build.gradle b/piano-dispatcher/build.gradle
new file mode 100644
index 0000000..c0bfe64
--- /dev/null
+++ b/piano-dispatcher/build.gradle
@@ -0,0 +1,63 @@
+plugins {
+ id 'com.android.library'
+}
+
+ext {
+ mavenGroupId = 'com.batch.android'
+ mavenArtifact = 'piano-dispatcher'
+ powerMockVersion = '2.0.7'
+}
+
+android {
+ namespace 'com.batch.android.dispatcher.piano'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 16
+ targetSdk 33
+ versionCode 1
+ versionName "1.0.0-rc.1"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ all {
+ testLogging {
+ events "started", "passed", "skipped", "failed"
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+
+ api 'com.batch.android:batch-sdk:1.19.3'
+ api 'io.piano:analytics:3.2.0'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'androidx.test.ext:junit:1.1.4'
+ testImplementation 'org.mockito:mockito-core:3.4.6'
+ testImplementation 'org.mockito:mockito-inline:3.4.6' // Mockito extension to mock final class
+ testImplementation 'org.robolectric:robolectric:4.7.1'
+ testImplementation 'androidx.test.espresso:espresso-core:3.5.0'
+ testImplementation "org.powermock:powermock-module-junit4:$powerMockVersion"
+ testImplementation "org.powermock:powermock-module-junit4-rule:$powerMockVersion"
+ testImplementation "org.powermock:powermock-api-mockito2:$powerMockVersion"
+ testImplementation "org.powermock:powermock-classloading-xstream:$powerMockVersion"
+}
+apply from: 'maven-publish.gradle'
\ No newline at end of file
diff --git a/piano-dispatcher/maven-publish.gradle b/piano-dispatcher/maven-publish.gradle
new file mode 100644
index 0000000..32acf1a
--- /dev/null
+++ b/piano-dispatcher/maven-publish.gradle
@@ -0,0 +1,164 @@
+apply plugin: 'maven-publish'
+apply plugin: 'signing'
+
+def propFile = project.rootProject.file("maven.properties")
+Properties props = new Properties()
+if (propFile.exists()) {
+ props.load(propFile.newDataInputStream())
+ allprojects {
+ ext."signing.password" = props.getProperty("batch.maven.central.signing.password", "")
+ }
+}
+
+def signingFile = project.rootProject.file("batch-maven-secret-gpg.key")
+if (signingFile.exists()) {
+ allprojects {
+ ext."signing.keyId" = "561B6D31"
+ ext."signing.secretKeyRingFile" = signingFile.getAbsolutePath()
+ }
+}
+
+task androidJavadocs(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+ android.libraryVariants.all { variant ->
+ if (variant.name == 'release') {
+ owner.classpath += variant.getCompileClasspath()
+ }
+ }
+ exclude '**/R.html', '**/R.*.html', '**/index.html'
+}
+
+task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
+ archiveClassifier.set("javadoc")
+ from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+ archiveClassifier.set("sources")
+ from android.sourceSets.main.java.srcDirs
+}
+
+publishing {
+ publications {
+ pianodispatcher(MavenPublication) {
+
+ groupId mavenGroupId
+ artifactId mavenArtifact
+ version android.defaultConfig.versionName
+
+ artifact "$buildDir/outputs/aar/piano-dispatcher-release.aar"
+ artifact androidJavadocsJar
+ artifact androidSourcesJar
+
+ pom {
+ name = "Batch-Android-piano-dispatcher"
+ packaging = "aar"
+ description = "Batch.com's Android Piano Analytics Dispatcher main artifact"
+ url = "https://batch.com"
+
+ scm {
+ url = "https://github.com/BatchLabs/Batch-Android-piano-dispatcher"
+ connection = "scm:git:https://github.com/BatchLabs/Batch-Android-piano-dispatcher.git"
+ developerConnection = "scm:git:https://github.com/BatchLabs/Batch-Android-piano-dispatcher.git"
+ }
+
+ licenses {
+ license {
+ name = "MIT License - Copyright (c) 2022-Present Batch.com"
+ distribution = "repo"
+ comments = "Permission is hereby granted, free of charge, to any person obtaining a copy\n" +
+ "of this software and associated documentation files (the \"Software\"), to deal\n" +
+ "in the Software without restriction, including without limitation the rights\n" +
+ "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" +
+ "copies of the Software, and to permit persons to whom the Software is\n" +
+ "furnished to do so, subject to the following conditions:\n" +
+ "\n" +
+ "The above copyright notice and this permission notice shall be included in\n" +
+ "all copies or substantial portions of the Software.\n" +
+ "\n" +
+ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" +
+ "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" +
+ "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" +
+ "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" +
+ "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" +
+ "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n" +
+ "THE SOFTWARE.\n"
+ }
+ }
+
+ developers {
+ developer {
+ id = "batch-tech"
+ name = "Batch.com Tech"
+ }
+ }
+
+ organization {
+ name = "Batch.com"
+ url = "https://batch.com"
+ }
+ }
+
+ pom.withXml {
+ final dependenciesNode = asNode().appendNode('dependencies')
+ ext.addDependency = { Dependency dep, String scope ->
+ if (dep.group == null || dep.version == null || dep.name == null || dep.name == "unspecified")
+ return // ignore invalid dependencies
+
+ final dependencyNode = dependenciesNode.appendNode('dependency')
+ dependencyNode.appendNode('groupId', dep.group)
+ dependencyNode.appendNode('artifactId', dep.name)
+ dependencyNode.appendNode('version', dep.version)
+ dependencyNode.appendNode('scope', scope)
+
+ if (!dep.transitive) {
+ // If this dependency is transitive, we should force exclude all its dependencies them from the POM
+ final exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion')
+ exclusionNode.appendNode('groupId', '*')
+ exclusionNode.appendNode('artifactId', '*')
+ } else if (!dep.properties.excludeRules.empty) {
+ // Otherwise add specified exclude rules
+ final exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion')
+ dep.properties.excludeRules.each { ExcludeRule rule ->
+ exclusionNode.appendNode('groupId', rule.group ?: '*')
+ exclusionNode.appendNode('artifactId', rule.module ?: '*')
+ }
+ }
+ }
+
+ configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") }
+ configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ if (!props.getProperty("batch.maven.central.release_repo_url", "").equals("")) {
+ url props.getProperty("batch.maven.central.release_repo_url", "")
+ credentials {
+ username = props.getProperty("batch.maven.central.username", "")
+ password = props.getProperty("batch.maven.central.password", "")
+ }
+ } else if (project.gradle.startParameter.taskNames.contains('publish')) {
+ logger.warn("WARNING: Could not get maven repository url, are you sure to have correctly setup " +
+ propFile.path + " and " +
+ signingFile.path + " ?")
+ }
+ }
+ }
+}
+
+signing {
+ sign publishing.publications.pianodispatcher
+}
+
+// Do not sign if we run the publishToMavenLocal task, useful for dev
+tasks.withType(Sign) {
+ onlyIf {
+ !project.gradle.startParameter.taskNames.contains('publishToMavenLocal') &&
+ !project.gradle.startParameter.taskNames.contains('sdk:publishToMavenLocal')
+ }
+}
+
diff --git a/piano-dispatcher/proguard-rules.pro b/piano-dispatcher/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/piano-dispatcher/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/piano-dispatcher/src/main/AndroidManifest.xml b/piano-dispatcher/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6deba06
--- /dev/null
+++ b/piano-dispatcher/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoDispatcher.java b/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoDispatcher.java
new file mode 100644
index 0000000..1329e05
--- /dev/null
+++ b/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoDispatcher.java
@@ -0,0 +1,477 @@
+package com.batch.android.dispatcher.piano;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.batch.android.Batch;
+import com.batch.android.BatchEventDispatcher;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import io.piano.analytics.Event;
+import io.piano.analytics.PianoAnalytics;
+
+/**
+ * Piano Event Dispatcher
+ *
+ * Dispatch Batch events to the Piano Analytics SDK. By default events are dispatched as On-site Ads.
+ * If you want to dispatch as custom event, please see {@link PianoDispatcher#enableBatchCustomEvents(boolean)}.
+ * Note: if you enable custom events, you need to declare them in your Piano Data Model.
+ */
+public class PianoDispatcher implements BatchEventDispatcher {
+
+ /**
+ * Batch internal dispatcher information used for analytics
+ */
+ private static final String DISPATCHER_NAME = "piano";
+ private static final int DISPATCHER_VERSION = 1;
+
+ /**
+ * Piano event keys
+ */
+ private static final String CAMPAIGN = "src_campaign";
+ private static final String SOURCE = "src_source";
+ private static final String SOURCE_FORCE = "src_force";
+ private static final String MEDIUM = "src_medium";
+ private static final String CONTENT = "src_content";
+
+ private static final String EVENT_IMPRESSION = "publisher.impression";
+ private static final String EVENT_CLICK = "publisher.click";
+
+ private static final String ON_SITE_TYPE = "onsitead_type";
+ private static final String ON_SITE_TYPE_PUBLISHER = "Publisher";
+ private static final String ON_SITE_ADVERTISER = "onsitead_advertiser";
+ private static final String ON_SITE_CAMPAIGN = "onsitead_campaign";
+ private static final String ON_SITE_FORMAT = "onsitead_format";
+
+ /**
+ * Custom event name used when logging on Piano
+ *
+ * Note: Must be add as custom event/property in your Piano Data Model
+ */
+ private static final String NOTIFICATION_DISPLAY_NAME = "batch_notification_display";
+ private static final String NOTIFICATION_OPEN_NAME = "batch_notification_open";
+ private static final String NOTIFICATION_DISMISS_NAME = "batch_notification_dismiss";
+ private static final String MESSAGING_SHOW_NAME = "batch_in_app_show";
+ private static final String MESSAGING_CLOSE_NAME = "batch_in_app_close";
+ private static final String MESSAGING_AUTO_CLOSE_NAME = "batch_in_app_auto_close";
+ private static final String MESSAGING_CLOSE_ERROR_NAME = "batch_in_app_close_error";
+ private static final String MESSAGING_CLICK_NAME = "batch_in_app_click";
+ private static final String MESSAGING_WEBVIEW_CLICK_NAME = "batch_in_app_webview_click";
+ private static final String BATCH_WEBVIEW_ANALYTICS_ID = "batch_webview_analytics_id";
+ private static final String BATCH_TRACKING_ID = "batch_tracking_id";
+ private static final String UNKNOWN_EVENT_NAME = "batch_unknown";
+
+ /**
+ * Batch event values
+ */
+ private static final String BATCH_SRC = "Batch";
+ private static final String BATCH_FORMAT_IN_APP = "in-app";
+ private static final String BATCH_FORMAT_PUSH = "push";
+ private static final String BATCH_DEFAULT_CAMPAIGN = "batch-default-campaign";
+
+ /**
+ * Third-party keys
+ */
+ private static final String AT_MEDIUM = "at_medium";
+ private static final String AT_CAMPAIGN = "at_campaign";
+
+ private static final String UTM_SOURCE = "utm_source";
+ private static final String UTM_MEDIUM = "utm_medium";
+ private static final String UTM_CAMPAIGN = "utm_campaign";
+ private static final String UTM_CONTENT = "utm_content";
+
+ /**
+ * Piano Analytics instance
+ */
+ private final PianoAnalytics pianoAnalytics;
+
+ /**
+ * Whether Batch should send custom events (default: false)
+ *
+ * Note: Custom events must be defined in the Piano Data Model
+ */
+ private boolean customEventsEnabled = false;
+
+ /**
+ * Whether Batch should handle UTM tags in campaign's deeplink
+ * and custom payload. (default = true)
+ */
+ private boolean isUTMTrackingEnabled = true;
+
+ /**
+ * Constructor
+ *
+ * @param context application context
+ */
+ public PianoDispatcher(@NonNull Context context) {
+ pianoAnalytics = PianoAnalytics.getInstance(context);
+ }
+
+ /**
+ * Whether Batch should dispatch as Piano Custom Event.
+ *
+ * Note: This method should be called the as soon as possible in your Application subclass
+ * just after your Piano Configuration.
+ *
+ * @param enabled true if you want to enable custom events
+ */
+ public void enableBatchCustomEvents(boolean enabled) {
+ this.customEventsEnabled = enabled;
+ }
+
+ /**
+ * Whether Batch should handle UTM tags in campaign's deeplink
+ * and custom payload. (default = true)
+ */
+ public void enableUTMTracking(boolean enabled) {
+ this.isUTMTrackingEnabled = enabled;
+ }
+
+ /**
+ * Get the analytics name of this dispatcher
+ *
+ * @return The name
+ */
+ @Override
+ public String getName() {
+ return DISPATCHER_NAME;
+ }
+
+ /**
+ * Get the analytics version of this dispatcher
+ *
+ * @return The version
+ */
+ @Override
+ public int getVersion() {
+ return DISPATCHER_VERSION;
+ }
+
+ /**
+ * Callback fired when a new Batch event is triggered
+ *
+ * @param type The type of the event
+ * @param payload The associated payload of the event
+ */
+ @Override
+ public void dispatchEvent(@NonNull Batch.EventDispatcher.Type type, @NonNull Batch.EventDispatcher.Payload payload) {
+
+ // Dispatch onSiteAds event
+ if (shouldBeDispatchedAsOnSiteAd(type)) {
+ Event onSiteAdsEvent = buildPianoOnSiteAdsEvent(type, payload);
+ if (onSiteAdsEvent != null) {
+ pianoAnalytics.sendEvent(onSiteAdsEvent);
+ }
+ }
+
+ // Dispatch Custom Event if enabled
+ if (customEventsEnabled) {
+ Event event = buildPianoCustomEvent(type, payload);
+ pianoAnalytics.sendEvent(event);
+ }
+ }
+
+ /**
+ * Build an On-Site Ads Piano Event from a Batch Event
+ *
+ * @param type Batch event type
+ * @param payload Batch event payload
+ * @return The Piano event to send
+ */
+ @Nullable
+ @VisibleForTesting
+ Event buildPianoOnSiteAdsEvent(Batch.EventDispatcher.Type type, Batch.EventDispatcher.Payload payload) {
+
+ String pianoOnSiteEventName;
+ if (isImpression(type)) {
+ pianoOnSiteEventName = EVENT_IMPRESSION;
+ } else if (isClick(type)) {
+ pianoOnSiteEventName = EVENT_CLICK;
+ } else {
+ return null;
+ }
+
+ HashMap params = new HashMap() {{
+ put(ON_SITE_TYPE, ON_SITE_TYPE_PUBLISHER);
+ put(ON_SITE_ADVERTISER, getSource(payload));
+ put(ON_SITE_CAMPAIGN, getCampaign(payload));
+ put(ON_SITE_FORMAT, getMedium(payload, type));
+ }};
+ return new Event(pianoOnSiteEventName, params);
+ }
+
+ /**
+ * Build a Piano Custom Event from a Batch Event
+ *
+ * @param type Batch event type
+ * @param payload Batch event payload
+ * @return The Piano event to send
+ */
+ @VisibleForTesting
+ Event buildPianoCustomEvent(@NonNull Batch.EventDispatcher.Type type,
+ @NonNull Batch.EventDispatcher.Payload payload) {
+
+ String eventName = getPianoEventName(type);
+ HashMap eventData = new HashMap() {{
+ put(CAMPAIGN, getCampaign(payload));
+ put(MEDIUM, getMedium(payload, type));
+ put(SOURCE, getSource(payload));
+ put(SOURCE_FORCE, true);
+ }};
+
+ String trackingId = payload.getTrackingId();
+ if (trackingId != null && !trackingId.isEmpty()) {
+ eventData.put(BATCH_TRACKING_ID, trackingId);
+ }
+
+ String content = getContent(payload);
+ if (content != null && !content.isEmpty()) {
+ eventData.put(CONTENT, content);
+ }
+
+ if (type.isMessagingEvent()) {
+ String webViewAnalyticsId = payload.getWebViewAnalyticsID();
+ if (webViewAnalyticsId != null && !webViewAnalyticsId.isEmpty()) {
+ eventData.put(BATCH_WEBVIEW_ANALYTICS_ID, webViewAnalyticsId);
+ }
+ }
+ return new Event(eventName, eventData);
+ }
+
+ /**
+ * Get the campaign label to send at Piano.
+ *
+ * First, check for an "at_campaign" tag in the custom payload, or in the deeplink.
+ * If not, check for an "utm_campaign" tag in the custom payload, or in the deeplink.
+ * If not, check if there is a TrackingID attached.
+ * If not, use {@link PianoDispatcher#BATCH_DEFAULT_CAMPAIGN}
+ *
+ * @param payload Batch event payload
+ * @return The campaign label.
+ */
+ @NonNull
+ @VisibleForTesting
+ String getCampaign(@NonNull Batch.EventDispatcher.Payload payload) {
+ String campaign = getTagFromPayload(payload, AT_CAMPAIGN);
+ if (campaign != null && !campaign.isEmpty()) {
+ return campaign;
+ }
+ campaign = getTagFromPayload(payload, UTM_CAMPAIGN);
+ if (isUTMTrackingEnabled && campaign != null && !campaign.isEmpty()) {
+ return campaign;
+ }
+ campaign = payload.getTrackingId();
+ if (campaign != null && !campaign.isEmpty()) {
+ return campaign;
+ }
+ return BATCH_DEFAULT_CAMPAIGN;
+ }
+
+ /**
+ * Get the medium
+ *
+ * Check for at_medium or utm_medium tags
+ * If not found return "push" or "in-app" according to the batch event type
+ *
+ * @param payload Batch event payload
+ * @param type Batch event type
+ * @return The medium
+ */
+ @NonNull
+ private String getMedium(@NonNull Batch.EventDispatcher.Payload payload, @NonNull Batch.EventDispatcher.Type type) {
+ String medium;
+ medium = getTagFromPayload(payload, AT_MEDIUM);
+ if (medium != null && !medium.isEmpty()) {
+ return medium;
+ }
+ medium = getTagFromPayload(payload, UTM_MEDIUM);
+ if (isUTMTrackingEnabled && medium != null && !medium.isEmpty()) {
+ return medium;
+ }
+ if (type.isNotificationEvent()) {
+ medium = BATCH_FORMAT_PUSH;
+ } else {
+ medium = BATCH_FORMAT_IN_APP;
+ }
+ return medium;
+ }
+
+ /**
+ * Get the source
+ *
+ * Check for utm_source tags
+ * If not found return "Batch"
+ *
+ * @param payload Batch event payload
+ * @return The source
+ */
+ @NonNull
+ private String getSource(@NonNull Batch.EventDispatcher.Payload payload) {
+ String source = getTagFromPayload(payload, UTM_SOURCE);
+ if (isUTMTrackingEnabled && source != null && !source.isEmpty()) {
+ return source;
+ }
+ return BATCH_SRC;
+ }
+
+ /**
+ * Get the content
+ *
+ * Check for utm_content tag
+ * If not found return null
+ *
+ * @param payload Batch event payload
+ * @return The content
+ */
+ @Nullable
+ private String getContent(@NonNull Batch.EventDispatcher.Payload payload) {
+ String content = getTagFromPayload(payload, UTM_CONTENT);
+ if (isUTMTrackingEnabled && content != null && !content.isEmpty()) {
+ return content;
+ }
+ return null;
+ }
+
+ /**
+ * Get the corresponding Piano event name.
+ *
+ * @param type Batch event type
+ * @return The corresponding event name for Piano
+ */
+ @NonNull
+ private String getPianoEventName(Batch.EventDispatcher.Type type) {
+ switch (type) {
+ case NOTIFICATION_DISPLAY:
+ return NOTIFICATION_DISPLAY_NAME;
+ case NOTIFICATION_OPEN:
+ return NOTIFICATION_OPEN_NAME;
+ case NOTIFICATION_DISMISS:
+ return NOTIFICATION_DISMISS_NAME;
+ case MESSAGING_SHOW:
+ return MESSAGING_SHOW_NAME;
+ case MESSAGING_CLOSE:
+ return MESSAGING_CLOSE_NAME;
+ case MESSAGING_AUTO_CLOSE:
+ return MESSAGING_AUTO_CLOSE_NAME;
+ case MESSAGING_CLOSE_ERROR:
+ return MESSAGING_CLOSE_ERROR_NAME;
+ case MESSAGING_CLICK:
+ return MESSAGING_CLICK_NAME;
+ case MESSAGING_WEBVIEW_CLICK:
+ return MESSAGING_WEBVIEW_CLICK_NAME;
+ }
+ return UNKNOWN_EVENT_NAME;
+ }
+
+ /**
+ * Indicate if an event type should be dispatched as On-site Ads
+ *
+ * @param type Batch event type
+ * @return True if this kind of event should be dispatched as On-site Ads.
+ */
+ private boolean shouldBeDispatchedAsOnSiteAd(Batch.EventDispatcher.Type type) {
+ return isImpression(type) || isClick(type);
+ }
+
+ /**
+ * Whether this kind of Batch event corresponds to a Piano publisher impression event
+ *
+ * @param type Batch event type
+ * @return True if it's an impression
+ */
+ private boolean isImpression(Batch.EventDispatcher.Type type) {
+ return type.equals(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY) ||
+ type.equals(Batch.EventDispatcher.Type.MESSAGING_SHOW);
+ }
+
+ /**
+ * Whether this kind of Batch event corresponds to a Piano publisher click event
+ *
+ * @param type Batch event type
+ * @return True if it's a click
+ */
+ private boolean isClick(Batch.EventDispatcher.Type type) {
+ return type.equals(Batch.EventDispatcher.Type.NOTIFICATION_OPEN) ||
+ type.equals(Batch.EventDispatcher.Type.MESSAGING_CLICK) ||
+ type.equals(Batch.EventDispatcher.Type.MESSAGING_WEBVIEW_CLICK);
+ }
+
+ /**
+ * Simple helper method to get a tag from a Batch event payload.
+ *
+ * @param payload Batch event payload
+ * @param tagName Tag name
+ * @return The tag
+ */
+ @Nullable
+ private String getTagFromPayload(@NonNull Batch.EventDispatcher.Payload payload, @NonNull String tagName) {
+ String tag = getTagFromDeeplink(payload, tagName);
+
+ String tagTmp = payload.getCustomValue(tagName);
+ if (tagTmp != null) {
+ tag = tagTmp;
+ }
+ return tag;
+ }
+
+ /**
+ * Simple helper method to get a tag from a Batch deeplink.
+ *
+ * @param payload Batch event payload
+ * @param tagName Tag name
+ * @return The tag
+ */
+ @Nullable
+ private String getTagFromDeeplink(Batch.EventDispatcher.Payload payload, String tagName) {
+ String tag = null;
+ String deeplink = payload.getDeeplink();
+ if (deeplink != null) {
+ deeplink = deeplink.trim();
+ tagName = tagName.toLowerCase();
+ Uri uri = Uri.parse(deeplink);
+ if (uri.isHierarchical()) {
+ String fragment = uri.getFragment();
+ if (fragment != null && !fragment.isEmpty()) {
+ Map fragments = getFragmentMap(fragment);
+ String tagTmp = fragments.get(tagName);
+ if (tagTmp != null) {
+ tag = tagTmp;
+ }
+ }
+ Set keys = uri.getQueryParameterNames();
+ for (String key : keys) {
+ if (tagName.equalsIgnoreCase(key)) {
+ return uri.getQueryParameter(key);
+ }
+ }
+ }
+ }
+ return tag;
+ }
+
+ /**
+ * Simple helper method to get a fragment as a map.
+ *
+ * @param fragment url fragment
+ * @return the map
+ */
+ @NonNull
+ private Map getFragmentMap(String fragment) {
+ String[] params = fragment.split("&");
+ Map map = new HashMap<>();
+ for (String param : params) {
+ String[] parts = param.split("=");
+ if (parts.length >= 2) {
+ map.put(parts[0].toLowerCase(), parts[1]);
+ }
+ }
+ return map;
+ }
+}
diff --git a/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoRegistrar.java b/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoRegistrar.java
new file mode 100644
index 0000000..f380d71
--- /dev/null
+++ b/piano-dispatcher/src/main/java/com/batch/android/dispatcher/piano/PianoRegistrar.java
@@ -0,0 +1,56 @@
+package com.batch.android.dispatcher.piano;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import com.batch.android.BatchEventDispatcher;
+import com.batch.android.eventdispatcher.DispatcherRegistrar;
+
+public class PianoRegistrar implements DispatcherRegistrar {
+
+ private static PianoDispatcher instance = null;
+
+ /**
+ * Meta-data name to enable custom events
+ */
+ private static final String CUSTOM_EVENT_ENABLED_METADATA = "com.batch.android.dispatcher.piano.enable_custom_events";
+
+ /**
+ * Meta-data name to enable utm tracking
+ */
+ private static final String UTM_TRACKING_ENABLED_METADATA = "com.batch.android.dispatcher.piano.enable_utm_tracking";
+
+ @Override
+ public BatchEventDispatcher getDispatcher(Context context) {
+ if (instance == null) {
+ instance = new PianoDispatcher(context);
+ instance.enableBatchCustomEvents(getBooleanMetaDataInfo(context, CUSTOM_EVENT_ENABLED_METADATA, false));
+ instance.enableUTMTracking(getBooleanMetaDataInfo(context, UTM_TRACKING_ENABLED_METADATA, true));
+ }
+ return instance;
+ }
+
+ /**
+ * Get boolean meta-data value from Android's manifest.
+ *
+ * @param context Application context
+ * @param key Name of the meta-data
+ * @param fallback Default value to fallback
+ * @return the value found or the fallback
+ */
+ private boolean getBooleanMetaDataInfo(Context context, String key, boolean fallback) {
+ try {
+ ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ if (appInfo.metaData != null) {
+ return appInfo.metaData.getBoolean(key, fallback);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // if we can’t find it in the manifest, just return the fallback
+ } catch (Exception e) {
+ Log.e("Batch", "Error while parsing meta-data info", e);
+ }
+ return fallback;
+ }
+}
diff --git a/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoDispatcherTest.java b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoDispatcherTest.java
new file mode 100644
index 0000000..97cfeb6
--- /dev/null
+++ b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoDispatcherTest.java
@@ -0,0 +1,316 @@
+package com.batch.android.dispatcher.piano;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.reflect.Whitebox;
+import org.robolectric.annotation.Config;
+
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.batch.android.Batch;
+
+import java.util.HashMap;
+
+import io.piano.analytics.Event;
+import io.piano.analytics.PianoAnalytics;
+
+@RunWith(AndroidJUnit4.class)
+@Config(sdk = Build.VERSION_CODES.S)
+@PowerMockIgnore({"org.powermock.*", "org.mockito.*", "org.robolectric.*", "android.*", "androidx.*"})
+@PrepareForTest(PianoAnalytics.class)
+public class PianoDispatcherTest {
+
+ private PianoDispatcher dispatcher;
+
+ @Before
+ public void setUp() {
+ this.dispatcher = new PianoDispatcher(ApplicationProvider.getApplicationContext());
+ }
+
+ @Test
+ public void testGetCampaignFromTrackingID() {
+ TestEventPayload payload = new TestEventPayload("from_tracking_id", "https://test.com", new Bundle());
+ Assert.assertEquals("from_tracking_id", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testGetCampaignFromCustomPayload() {
+ Bundle customPayload = new Bundle();
+ customPayload.putString("at_campaign", "from_payload");
+ TestEventPayload payload = new TestEventPayload(null, "https://test.com?at_campaign=from_deeplink", customPayload);
+ Assert.assertEquals("from_payload", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testGetCampaignFromDeeplink() {
+ TestEventPayload payload = new TestEventPayload(null, "https://test.com?at_campaign=from_deeplink", null);
+ Assert.assertEquals("from_deeplink", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testGetCampaignFromWrongDeeplink() {
+ TestEventPayload payload = new TestEventPayload(null, "mailto:test@test.com?utm_campaign=wrong", null);
+ Assert.assertEquals("batch-default-campaign", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testGetCampaignFromUTMCustomPayload() {
+ TestEventPayload payload = new TestEventPayload(null, "https://test.com?utm_campaign=expected", null);
+ Assert.assertEquals("expected", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testGetCampaignFromUTMDeepLink() {
+ Bundle customPayload = new Bundle();
+ customPayload.putString("utm_campaign", "expected");
+ TestEventPayload payload = new TestEventPayload(null, null, customPayload);
+ Assert.assertEquals("expected", dispatcher.getCampaign(payload));
+ }
+
+ @Test
+ public void testBuildPianoCustomEventInApp() {
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.MESSAGING_SHOW, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "in-app");
+ put("batch_tracking_id", "campaign_label");
+
+ }};
+ Assert.assertEquals("batch_in_app_show", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventPush() {
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "push");
+ put("batch_tracking_id", "campaign_label");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventFromDeeplinkAT() {
+ TestEventPayload payload = new TestEventPayload("batchTrackingId", "https://test.com?at_campaign=campaign_label&at_medium=email", null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventFromDeeplinkFirebase() {
+ TestEventPayload payload = new TestEventPayload("batchTrackingId", "https://test.com?utm_campaign=campaign_label&utm_medium=email&utm_source=firebase&utm_content=content", null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "firebase");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("src_content", "content");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventFromDeeplinkFirebaseFragment() {
+ TestEventPayload payload = new TestEventPayload("batchTrackingId",
+ "https://test.com#utm_campaign=campaign_label&utm_medium=email&utm_source=firebase&utm_content=content",
+ null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "firebase");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("src_content", "content");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testPriorityBuildPianoCustomEventFromDeeplink() {
+ TestEventPayload payload = new TestEventPayload("batchTrackingId",
+ "https://test.com?utm_campaign=campaign_label#utm_campaign=campaign_label2&utm_medium=email&utm_source=firebase&utm_content=content",
+ null);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "firebase");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("src_content", "content");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventFromPayloadAT() {
+ Bundle customPayload = new Bundle();
+ customPayload.putString("at_campaign", "campaign_label");
+ customPayload.putString("at_medium", "email");
+ TestEventPayload payload = new TestEventPayload("batchTrackingId", null, customPayload);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoCustomEventFromPayloadFirebase() {
+ Bundle customPayload = new Bundle();
+ customPayload.putString("utm_campaign", "campaign_label");
+ customPayload.putString("utm_medium", "email");
+ customPayload.putString("utm_source", "firebase");
+ customPayload.putString("utm_content", "content");
+ TestEventPayload payload = new TestEventPayload("batchTrackingId", null, customPayload);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "firebase");
+ put("src_force", true);
+ put("src_medium", "email");
+ put("src_content", "content");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testUTMTrackingDisabled() {
+ Bundle customPayload = new Bundle();
+ customPayload.putString("utm_campaign", "campaign_label");
+ customPayload.putString("utm_medium", "email");
+ customPayload.putString("utm_source", "firebase");
+ customPayload.putString("utm_content", "content");
+ TestEventPayload payload = new TestEventPayload("batchTrackingId", null, customPayload);
+ dispatcher.enableUTMTracking(false);
+ Event event = dispatcher.buildPianoCustomEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("src_campaign", "batchTrackingId");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "push");
+ put("batch_tracking_id", "batchTrackingId");
+ }};
+ Assert.assertEquals("batch_notification_display", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoOnSiteAdsEventImpressionPush() {
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+ Event event = dispatcher.buildPianoOnSiteAdsEvent(Batch.EventDispatcher.Type.NOTIFICATION_DISPLAY, payload);
+ HashMap expectedData = new HashMap() {{
+ put("onsitead_type", "Publisher");
+ put("onsitead_advertiser", "Batch");
+ put("onsitead_campaign", "campaign_label");
+ put("onsitead_format", "push");
+ }};
+ assert event != null;
+ Assert.assertEquals("publisher.impression", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testBuildPianoOnSiteAdsEventClickInApp() {
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+ Event event = dispatcher.buildPianoOnSiteAdsEvent(Batch.EventDispatcher.Type.MESSAGING_CLICK, payload);
+ HashMap expectedData = new HashMap() {{
+ put("onsitead_type", "Publisher");
+ put("onsitead_advertiser", "Batch");
+ put("onsitead_campaign", "campaign_label");
+ put("onsitead_format", "in-app");
+ }};
+ assert event != null;
+ Assert.assertEquals("publisher.click", event.getName());
+ Assert.assertEquals(expectedData, event.getData());
+ }
+
+ @Test
+ public void testDispatchEventWithoutCustomEvents() {
+ PianoAnalytics pa = PowerMockito.mock(PianoAnalytics.class);
+ Whitebox.setInternalState(dispatcher, "pianoAnalytics", pa);
+
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+ HashMap expectedData = new HashMap() {{
+ put("onsitead_type", "Publisher");
+ put("onsitead_advertiser", "Batch");
+ put("onsitead_campaign", "campaign_label");
+ put("onsitead_format", "in-app");
+ }};
+ Event expectedEvent = new Event("publisher.click", expectedData);
+ dispatcher.dispatchEvent(Batch.EventDispatcher.Type.MESSAGING_CLICK, payload);
+ Mockito.verify(pa, Mockito.times(1)).sendEvent(Mockito.any());
+ Mockito.verify(pa, Mockito.times(1)).sendEvent(PianoEventMockitoMatcher.eq(expectedEvent));
+ }
+
+ @Test
+ public void testDispatchEventWithCustomEvents() {
+ PianoAnalytics pa = PowerMockito.mock(PianoAnalytics.class);
+ Whitebox.setInternalState(dispatcher, "pianoAnalytics", pa);
+
+ TestEventPayload payload = new TestEventPayload("campaign_label", null, null);
+
+ HashMap onSiteEventExpectedData = new HashMap() {{
+ put("onsitead_type", "Publisher");
+ put("onsitead_advertiser", "Batch");
+ put("onsitead_campaign", "campaign_label");
+ put("onsitead_format", "in-app");
+ }};
+ Event expectedOnSiteAdsEvent = new Event("publisher.impression", onSiteEventExpectedData);
+
+ HashMap customEventExpectedData = new HashMap() {{
+ put("src_campaign", "campaign_label");
+ put("src_source", "Batch");
+ put("src_force", true);
+ put("src_medium", "in-app");
+ put("batch_tracking_id", "campaign_label");
+ }};
+ Event expectedCustomEvent = new Event("batch_in_app_show", customEventExpectedData);
+
+ dispatcher.enableBatchCustomEvents(true);
+ dispatcher.dispatchEvent(Batch.EventDispatcher.Type.MESSAGING_SHOW, payload);
+ Mockito.verify(pa, Mockito.times(2)).sendEvent(Mockito.any());
+ Mockito.verify(pa, Mockito.times(1)).sendEvent(PianoEventMockitoMatcher.eq(expectedOnSiteAdsEvent));
+ Mockito.verify(pa, Mockito.times(1)).sendEvent(PianoEventMockitoMatcher.eq(expectedCustomEvent));
+ }
+}
\ No newline at end of file
diff --git a/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoEventMockitoMatcher.java b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoEventMockitoMatcher.java
new file mode 100644
index 0000000..cdf2e69
--- /dev/null
+++ b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/PianoEventMockitoMatcher.java
@@ -0,0 +1,38 @@
+package com.batch.android.dispatcher.piano;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.batch.android.json.JSONObject;
+
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+import io.piano.analytics.Event;
+
+public class PianoEventMockitoMatcher implements ArgumentMatcher {
+
+ private final Event expected;
+
+ public PianoEventMockitoMatcher(@NonNull Event expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean matches(Event argument) {
+
+ if (argument == expected) {
+ return true;
+ }
+
+ if (argument.equals(expected)) {
+ return true;
+ }
+
+ return expected.getName().equals(argument.getName()) && expected.getData().equals(argument.getData());
+ }
+
+ public static Event eq(@NonNull Event expected) {
+ return Mockito.argThat(new PianoEventMockitoMatcher(expected));
+ }
+}
diff --git a/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/TestEventPayload.java b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/TestEventPayload.java
new file mode 100644
index 0000000..0b6c86a
--- /dev/null
+++ b/piano-dispatcher/src/test/java/com/batch/android/dispatcher/piano/TestEventPayload.java
@@ -0,0 +1,96 @@
+package com.batch.android.dispatcher.piano;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.batch.android.Batch;
+import com.batch.android.BatchMessage;
+import com.batch.android.BatchPushPayload;
+
+public class TestEventPayload implements Batch.EventDispatcher.Payload {
+
+ private final String trackingId;
+ private final String deeplink;
+ private final String webViewAnalyticsID;
+ private final Bundle customPayload;
+ private final boolean isPositive;
+
+ TestEventPayload(String trackingId,
+ String deeplink,
+ Bundle customPayload)
+ {
+ this(trackingId, null, deeplink, customPayload, false);
+ }
+
+ TestEventPayload(String trackingId,
+ String webViewAnalyticsID,
+ String deeplink,
+ Bundle customPayload)
+ {
+ this(trackingId, webViewAnalyticsID, deeplink, customPayload, false);
+ }
+
+ TestEventPayload(String trackingId,
+ String webViewAnalyticsID,
+ String deeplink,
+ Bundle customPayload,
+ boolean isPositive)
+ {
+ this.trackingId = trackingId;
+ this.webViewAnalyticsID = webViewAnalyticsID;
+ this.deeplink = deeplink;
+ this.customPayload = customPayload;
+ this.isPositive = isPositive;
+ }
+
+ @Nullable
+ @Override
+ public String getTrackingId()
+ {
+ return trackingId;
+ }
+
+ @Nullable
+ @Override
+ public String getDeeplink()
+ {
+ return deeplink;
+ }
+
+ @Override
+ public boolean isPositiveAction()
+ {
+ return isPositive;
+ }
+
+ @Nullable
+ @Override
+ public String getCustomValue(@NonNull String key)
+ {
+ if (customPayload == null) {
+ return null;
+ }
+ return customPayload.getString(key);
+ }
+
+ @Nullable
+ @Override
+ public BatchMessage getMessagingPayload()
+ {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public BatchPushPayload getPushPayload()
+ {
+ return null;
+ }
+
+ @Nullable
+ public String getWebViewAnalyticsID() {
+ return webViewAnalyticsID;
+ }
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..280f452
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "piano-dispatcher"
+include ':piano-dispatcher'