diff --git a/Application/app/build.gradle b/Application/app/build.gradle index edb4d04..5e5f689 100644 --- a/Application/app/build.gradle +++ b/Application/app/build.gradle @@ -21,6 +21,9 @@ apply from: "../artifacts.gradle" repositories { // Uncomment when using CrashlyticsComponent //maven { url 'https://maven.fabric.io/public' } + +// Uncomment when using TealiumAnalyticsComponent +// maven { url "http://maven.tealiumiq.com/android/releases/" } } buildscript { @@ -83,10 +86,10 @@ dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile 'junit:junit:4.12' androidTestCompile 'org.mockito:mockito-core:1.9.5' - androidTestCompile ('com.android.support.test:rules:0.5') { + androidTestCompile('com.android.support.test:rules:0.5') { exclude group: 'com.android.support', module: 'support-annotations' } - androidTestCompile ('com.android.support.test:runner:0.5') { + androidTestCompile('com.android.support.test:runner:0.5') { exclude group: 'com.android.support', module: 'support-annotations' } androidTestCompile 'com.google.dexmaker:dexmaker:1.2' @@ -100,4 +103,6 @@ dependencies { compile project(':PassThroughAdsComponent') compile project(':PassThroughLoginComponent') compile project(':LoggerAnalyticsComponent') +// Uncomment when using TealiumAnalyticsComponent +// compile project(':TealiumAnalyticsComponent') } diff --git a/Application/settings.gradle b/Application/settings.gradle index 4912b06..6db69ed 100644 --- a/Application/settings.gradle +++ b/Application/settings.gradle @@ -24,7 +24,7 @@ include ':app', ':DataLoader', ':Utils', // Uncomment when using ComScore Analytics - //':comscore', +// ':comscore', /* Interfaces */ ':PurchaseInterface', ':AuthInterface', @@ -36,6 +36,8 @@ include ':app', ':AMZNMediaPlayerComponent', ':PassThroughLoginComponent', ':LoggerAnalyticsComponent' + // Uncomment when using Tealium Analytics +// ':TealiumAnalyticsComponent' /* Frameworks */ project(':TVUIComponent').projectDir = new File(rootProject.projectDir, '../TVUIComponent/lib') @@ -63,3 +65,5 @@ project(':PassThroughLoginComponent').projectDir = new File(rootProject.projectD project(':LoggerAnalyticsComponent').projectDir = new File(rootProject.projectDir, '../LoggerAnalyticsComponent') // Uncomment when using ComScore Analytics //project(':comscore').projectDir = new File(rootProject.projectDir, '../ComScoreAnalyticsComponent/libs/comscore') +// Uncomment when using Tealium Analytics +//project(':TealiumAnalyticsComponent').projectDir = new File(rootProject.projectDir, '../TealiumAnalyticsComponent') diff --git a/TealiumAnalyticsComponent/build.gradle b/TealiumAnalyticsComponent/build.gradle new file mode 100644 index 0000000..b2c0b12 --- /dev/null +++ b/TealiumAnalyticsComponent/build.gradle @@ -0,0 +1,60 @@ +apply plugin: 'com.android.library' + +repositories { + mavenCentral() + + maven { + url "http://maven.tealiumiq.com/android/releases/" + } +} + +android { + compileSdkVersion 28 + buildToolsVersion "28.0.0" + + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':ModuleInterface') + compile project(':AnalyticsInterface') + + testCompile('org.robolectric:robolectric:3.0') { + exclude group: 'commons-logging', module: 'commons-logging' + exclude group: 'org.apache.httpcomponents', module: 'httpclient' + } + + compile 'com.tealium:library:5.5.1' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.powermock:powermock:1.6.6' + testCompile 'org.powermock:powermock-module-junit4:1.6.6' + testCompile 'org.powermock:powermock-module-junit4-rule:1.6.6' + testCompile 'org.powermock:powermock-api-mockito:1.6.6' + testCompile 'org.powermock:powermock-classloading-xstream:1.6.6' + + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'com.android.support.test:rules:0.5' + androidTestCompile('com.android.support.test:runner:0.5') { + exclude module: 'support-annotations' + } + +} diff --git a/TealiumAnalyticsComponent/src/main/AndroidManifest.xml b/TealiumAnalyticsComponent/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f0cfa17 --- /dev/null +++ b/TealiumAnalyticsComponent/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalytics.java b/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalytics.java new file mode 100644 index 0000000..f4ca323 --- /dev/null +++ b/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalytics.java @@ -0,0 +1,121 @@ +package com.amazon.analytics.tealium; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.util.Log; + +import com.amazon.analytics.AnalyticsTags; +import com.amazon.analytics.CustomAnalyticsTags; +import com.amazon.analytics.IAnalytics; +import com.tealium.library.Tealium; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import static com.tealium.library.DataSources.Key.VIDEO_ID; +import static com.tealium.library.DataSources.Key.VIDEO_LENGTH; +import static com.tealium.library.DataSources.Key.VIDEO_MILESTONE; +import static com.tealium.library.DataSources.Key.VIDEO_NAME; +import static com.tealium.library.DataSources.Key.VIDEO_PLAYHEAD; + +public class TealiumAnalytics implements IAnalytics { + + private static final String TAG = TealiumAnalytics.class.getSimpleName(); + private Tealium mTealium; + private CustomAnalyticsTags mCustomAnalyticsTags = new CustomAnalyticsTags(); + + /** + * {@inheritDoc} + */ + @Override + public void configure(Context context) { + Application application = (Application) context.getApplicationContext(); + Tealium.Config config = Tealium.Config.create(application, "", "", ""); + mTealium = Tealium.createInstance("", config); + + Log.d(TAG, "Tealium Analytics initialized"); + } + + /** + * {@inheritDoc} + */ + @Override + public void collectLifeCycleData(Activity activity, boolean active) { + Log.d(TAG, "Lifecycle is not supported for this platform."); + } + + /** + * {@inheritDoc} + * + * @param data Map of Strings to Objects that represent data that is necessary for the tracked + */ + @Override + public void trackAction(HashMap data) { + HashMap contextData = new HashMap<>(); + + // Get the action name + String action = String.valueOf(data.get(AnalyticsTags.ACTION_NAME)); + contextData.put(AnalyticsTags.ACTION_NAME, action); + + // Get the attributes map + HashMap contextDataObjectMap = (HashMap) data.get(AnalyticsTags.ATTRIBUTES); + + if (action != null && contextDataObjectMap != null) { + for (String key : contextDataObjectMap.keySet()) { + String value = String.valueOf(contextDataObjectMap.get(key)); + Long videoDuration; + + switch (key) { + case AnalyticsTags.ATTRIBUTE_VIDEO_CURRENT_POSITION: + contextData.put(VIDEO_PLAYHEAD, value); + break; + case AnalyticsTags.ATTRIBUTE_VIDEO_ID: + contextData.put(VIDEO_ID, value); + break; + case AnalyticsTags.ATTRIBUTE_VIDEO_DURATION: + videoDuration = (Long) contextDataObjectMap.get(AnalyticsTags.ATTRIBUTE_VIDEO_DURATION); + contextData.put(VIDEO_LENGTH, getVideoDuration(videoDuration)); + break; + case AnalyticsTags.ATTRIBUTE_VIDEO_SECONDS_WATCHED: + videoDuration = (Long) contextDataObjectMap.get(AnalyticsTags.ATTRIBUTE_VIDEO_DURATION); + if (videoDuration != null) { + contextData.put(VIDEO_MILESTONE, getMilestone(videoDuration, Long.valueOf(value))); + } + break; + case AnalyticsTags.ATTRIBUTE_TITLE: + contextData.put(VIDEO_NAME, value); + break; + default: + contextData.put(key, value); + break; + } + } + + mTealium.trackEvent(mCustomAnalyticsTags.getCustomTag(action), + mCustomAnalyticsTags.getCustomTags(contextData)); + Log.d(TAG, "Track action " + action + "with attributes " + contextData); + } + } + + @Override + public void trackState(String screen) { + mTealium.trackView(screen, null); + Log.d(TAG, "Track screen: " + screen); + } + + @Override + public void trackCaughtError(String errorMessage, Throwable t) { + mTealium.trackEvent(errorMessage, null); + Log.d(TAG, "Tracking caught error: " + errorMessage); + } + + private String getVideoDuration(Long videoDuration) { + return String.valueOf(TimeUnit.MILLISECONDS.toSeconds(videoDuration)); + } + + private String getMilestone(Long videoDuration, Long secondsWatched) { + int percentage = (int) ((double) secondsWatched / videoDuration * 100); + return String.valueOf(percentage); + } +} diff --git a/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreator.java b/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreator.java new file mode 100644 index 0000000..60af3b5 --- /dev/null +++ b/TealiumAnalyticsComponent/src/main/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreator.java @@ -0,0 +1,15 @@ +package com.amazon.analytics.tealium; + +import com.amazon.analytics.IAnalytics; +import com.amazon.android.module.IImplCreator; + +public class TealiumAnalyticsImplCreator implements IImplCreator { + + /** + * {@inheritDoc} + */ + @Override + public IAnalytics createImpl() { + return new TealiumAnalytics(); + } +} diff --git a/TealiumAnalyticsComponent/src/main/res/values/strings.xml b/TealiumAnalyticsComponent/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/TealiumAnalyticsComponent/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreatorTest.java b/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreatorTest.java new file mode 100644 index 0000000..4b2648c --- /dev/null +++ b/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsImplCreatorTest.java @@ -0,0 +1,33 @@ +package com.amazon.analytics.tealium; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertTrue; + +/** + * Tests the {@link TealiumAnalyticsImplCreator} class + */ +@RunWith(RobolectricTestRunner.class) +public class TealiumAnalyticsImplCreatorTest { + + private TealiumAnalyticsImplCreator mTealiumAnalyticsImplCreator; + + @Before + public void setUp() { + mTealiumAnalyticsImplCreator = new TealiumAnalyticsImplCreator(); + } + + @After + public void tearDown() { + mTealiumAnalyticsImplCreator = null; + } + + @Test + public void createImpl() { + assertTrue("createImpl() should create a TealiumAnalyticsObject", mTealiumAnalyticsImplCreator.createImpl() instanceof TealiumAnalytics); + } +} diff --git a/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsTest.java b/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsTest.java new file mode 100644 index 0000000..3285193 --- /dev/null +++ b/TealiumAnalyticsComponent/src/test/java/com/amazon/analytics/tealium/TealiumAnalyticsTest.java @@ -0,0 +1,122 @@ +package com.amazon.analytics.tealium; + +import android.app.Application; +import android.content.Context; +import android.content.res.AssetManager; + +import com.amazon.analytics.AnalyticsTags; +import com.tealium.library.Tealium; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashMap; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Test suite for the {@link TealiumAnalytics} class + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21, manifest = Config.NONE) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*"}) +@PrepareForTest({com.tealium.library.Tealium.Config.class, com.tealium.library.Tealium.class}) +public class TealiumAnalyticsTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Mock + AssetManager mAssetManager; + + @Mock + Context mContext; + + @Mock + Application mApplication; + + @Mock + Tealium mTealium; + + @Mock + com.tealium.library.Tealium.Config mConfig; + + private TealiumAnalytics mTealiumAnalytics; + + @Before + public void setUp() { + mTealiumAnalytics = new TealiumAnalytics(); + initMocks(this); + PowerMockito.mockStatic(com.tealium.library.Tealium.Config.class); + PowerMockito.mockStatic(com.tealium.library.Tealium.class); + doReturn(mApplication).when(mContext).getApplicationContext(); + when(mContext.getAssets()).thenReturn(mAssetManager); + when(Tealium.Config.create(any(Application.class), anyString(), anyString(), anyString())).thenReturn(mConfig); + when(Tealium.createInstance(anyString(), any(Tealium.Config.class))).thenReturn(mTealium); + } + + @After + public void tearDown() { + mTealiumAnalytics = null; + } + + @Test + public void configure() { + mTealiumAnalytics.configure(mContext); + + assertNotNull(mTealiumAnalytics); + } + + @Test + public void trackAction() { + mTealiumAnalytics.configure(mContext); + final String TEST_ACTION = "testAction"; + HashMap dummyMap = new HashMap<>(); + HashMap contextData = new HashMap(); + + dummyMap.put(AnalyticsTags.ACTION_NAME, TEST_ACTION); + contextData.put(AnalyticsTags.ACTION_NAME, TEST_ACTION); + + contextData.put("key", "value"); + dummyMap.put(AnalyticsTags.ATTRIBUTES, contextData); + + mTealiumAnalytics.trackAction(dummyMap); + + verify(mTealium, times(1)).trackEvent(TEST_ACTION, contextData); + } + + @Test + public void trackState() { + mTealiumAnalytics.configure(mContext); + String screenName = "screenName"; + mTealiumAnalytics.trackState(screenName); + + verify(mTealium, times(1)).trackView(screenName, null); + } + + @Test + public void trackCaughtError() { + mTealiumAnalytics.configure(mContext); + mTealiumAnalytics.trackCaughtError("error", new Exception()); + + verify(mTealium, times(1)).trackEvent("error", null); + } + +} \ No newline at end of file