diff --git a/README.md b/README.md index 8bee511..7e70552 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ -analytics-android-integration-quantcast +analytics-android-integration-firebase ======================================= -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast) -[![Javadocs](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast.svg?label=javadoc)](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast) +[![Maven Central]() +[![Javadocs]() -Quantcast integration for [analytics-android](https://github.com/segmentio/analytics-android). +Firebase Analytics integration for [analytics-android](https://github.com/segmentio/analytics-android). ## Installation -To install the Segment-Quantcast integration, simply add this line to your gradle file: +To install the Segment-Firebase integration, simply add this line to your gradle file: ``` -compile 'com.segment.analytics.android.integrations:quantcast:+' +compile 'com.segment.analytics.android.integrations:firebase-analytics:+' ``` ## Usage -After adding the dependency, you must register the integration with our SDK. To do this, import the Quantcast integration: +After adding the dependency, you must register the integration with our SDK. To do this, import the Firebase integration: ``` -import com.segment.analytics.android.integrations.quantcast.QuantcastIntegration; +import com.segment.analytics.android.integrations.firebase-analytics.FirebaseIntegration; ``` @@ -28,11 +28,11 @@ And add the following line: ``` analytics = new Analytics.Builder(this, "write_key") - .use(QuantcastIntegration.FACTORY) + .use(FirebaseIntegration.FACTORY) .build(); ``` -Please see [our documentation](https://segment.com/docs/integrations/quantcast/) for more information. +Please see [our documentation](https://segment.com/docs/integrations/firebase-analytics/) for more information. ## License diff --git a/build.gradle b/build.gradle index 2fcab7f..64d127f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,26 +1,25 @@ buildscript { repositories { - mavenCentral() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' - classpath 'com.f2prateek.checkstyle:checkstyle:1.0.1' + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.f2prateek.javafmt:javafmt:0.1.6' } } apply plugin: 'com.android.library' -apply plugin: 'checkstyle' -apply plugin: 'com.f2prateek.checkstyle' +apply plugin: 'com.f2prateek.javafmt' android { - compileSdkVersion 23 - buildToolsVersion '23.0.1' + compileSdkVersion 26 + buildToolsVersion '25.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 23 - compileSdkVersion 23 + targetSdkVersion 26 + compileSdkVersion 26 } compileOptions { @@ -28,43 +27,39 @@ android { targetCompatibility JavaVersion.VERSION_1_7 } - lintOptions { - // Okio https://cloudup.com/cp7bi10o2C3g - disable 'InvalidPackage' + testOptions { + unitTests.returnDefaultValues = true } } -checkstyle { - configFile rootProject.file('gradle/checkstyle.xml') -} - dependencies { repositories { - mavenCentral() + jcenter() + maven { + url "https://maven.google.com/" + } } - provided 'com.segment.analytics.android:analytics:4.0.9' - compile 'com.google.firebase:firebase-core:11.2.0' + compile 'com.segment.analytics.android:analytics:4.2.6' + + testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' + testCompile 'junit:junit:4.12' - testCompile('org.robolectric:robolectric:3.0') { - exclude group: 'commons-logging', module: 'commons-logging' - exclude group: 'org.apache.httpcomponents', module: 'httpclient' - } - testCompile 'com.segment.analytics.android:analytics-tests:4.0.9' + testCompile 'org.robolectric:robolectric:3.4.2' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.powermock:powermock:1.6.2' - testCompile 'org.powermock:powermock-module-junit4:1.6.2' - testCompile 'org.powermock:powermock-module-junit4-rule:1.6.2' - testCompile 'org.powermock:powermock-api-mockito:1.6.2' - testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' + 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' } +apply plugin: 'com.f2prateek.javafmt' apply from: rootProject.file('gradle/gradle-mvn-push.gradle') -apply plugin: 'com.google.gms.google-services' diff --git a/gradle.properties b/gradle.properties index 17468b0..99c9d8c 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,18 @@ GROUP=com.segment.analytics.android.integrations -VERSION_CODE= -VERSION_NAME= +VERSION_CODE=001 +VERSION_NAME=0.0.1-SNAPSHOT -POM_ARTIFACT_ID= -POM_PACKAGING=jar +POM_ARTIFACT_ID=firebase +POM_PACKAGING=aar -POM_NAME= -POM_DESCRIPTION= +POM_NAME=Firebase Analytics Integration +POM_DESCRIPTION=Firebase Analytics Integration for Segment Android Analytics -POM_URL= -POM_SCM_URL= -POM_SCM_CONNECTION= -POM_SCM_DEV_CONNECTION= +POM_URL=http://github.com/segment-integrations/analytics-android-integration-firebase +POM_SCM_URL=http://github.com/segment-integrations/analytics-android-integration-firebase +POM_SCM_CONNECTION=scm:git:git://github.com/segment-integrations/analytics-android-integration-firebase.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/segment-integrations/analytics-android-integration-firebase.git POM_LICENCE_NAME=The MIT License (MIT) POM_LICENCE_URL=http://opensource.org/licenses/MIT diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1d242e1..ef49179 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Dec 26 13:17:50 MST 2014 +#Wed Aug 23 10:10:41 PDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 5f84218..9c08ff9 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1 +1,10 @@ - + + + + + + + + + diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index caf2054..935d05c 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -3,23 +3,32 @@ import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; -import com.google.firebase.analytics.FirebaseAnalytics; // FirebaseAnalytics class +import com.google.firebase.analytics.FirebaseAnalytics; +import com.google.firebase.analytics.FirebaseAnalytics.Event; +import com.google.firebase.analytics.FirebaseAnalytics.Param; import com.segment.analytics.Analytics; +import com.segment.analytics.Properties; import com.segment.analytics.ValueMap; import com.segment.analytics.integrations.IdentifyPayload; import com.segment.analytics.integrations.Integration; import com.segment.analytics.integrations.Logger; -import com.segment.analytics.integrations.ScreenPayload; import com.segment.analytics.integrations.TrackPayload; +import java.util.HashMap; +import java.util.Map; +import java.util.Date; + import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; -import static com.segment.analytics.Analytics.LogLevel.VERBOSE; +import static com.segment.analytics.internal.Utils.toISO8601Date; /** - * Google Analytics for Firebase is a free app measurement solution that provides insight on - * app usage and user engagement. + * Google Analytics for Firebase is a free app measurement solution that provides insight on app + * usage and user engagement. * * @see Google Analytics for Firebase * @see Google Analytics for Firebase Integration @@ -27,64 +36,208 @@ */ public class FirebaseIntegration extends Integration { - public static FirebaseAnalytics mFirebaseAnalytics; + public static final Factory FACTORY = + new Factory() { + @Override + public Integration create(ValueMap settings, Analytics analytics) { + Logger logger = analytics.logger(FIREBASE_ANALYTICS_KEY); + if (!hasPermission( + analytics.getApplication(), Manifest.permission.ACCESS_NETWORK_STATE)) { + logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics."); + return null; + } + if (!hasPermission(analytics.getApplication(), Manifest.permission.WAKE_LOCK)) { + logger.debug("WAKE_LOCK is required for Firebase Analytics."); + return null; + } + + Context context = analytics.getApplication(); + + return new FirebaseIntegration(context, logger); + } + + @Override + public String key() { + return FIREBASE_ANALYTICS_KEY; + } + }; + + private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; + private static final Map eventMapper = createEventMap(); + + private static Map createEventMap() { + Map eventMapper = new HashMap<>(); + eventMapper.put("Product Added", Event.ADD_TO_CART); + eventMapper.put("Checkout Started", Event.BEGIN_CHECKOUT); + eventMapper.put("Order Completed", Event.ECOMMERCE_PURCHASE); + eventMapper.put("Order Refunded", Event.PURCHASE_REFUND); + eventMapper.put("Product Viewed", Event.VIEW_ITEM); + eventMapper.put("Product List Viewed", Event.VIEW_ITEM_LIST); + eventMapper.put("Payment Info Entered", Event.ADD_PAYMENT_INFO); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + eventMapper.put("Product Added to Wishlist", Event.ADD_TO_WISHLIST); + eventMapper.put("Product Shared", Event.SHARE); + eventMapper.put("Product Clicked", Event.SELECT_CONTENT); + eventMapper.put("Product Searched", Event.SEARCH); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + return eventMapper; + } - public static final Factory FACTORY = new Factory() { - @Override public Integration create(ValueMap settings, Analytics analytics) { - Logger logger = analytics.logger(FIREBASE_ANALYTICS_KEY); - if (!hasPermission(analytics.getApplication(), Manifest.permission.ACCESS_NETWORK_STATE)) { - logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics."); - return null; - } - if (!hasPermission(analytics.getApplication(), Manifest.permission.INTERNET)) { - logger.debug("INTERNET is required for Firebase Analytics."); - return null; - } + private static final Map propertyMapper = createPropertyMap(); + + private static Map createPropertyMap() { + Map propertyMapper = new HashMap<>(); + propertyMapper.put("category", Param.ITEM_CATEGORY); + propertyMapper.put("product_id", Param.ITEM_ID); + propertyMapper.put("name", Param.ITEM_NAME); + propertyMapper.put("price", Param.PRICE); + propertyMapper.put("quantity", Param.QUANTITY); + propertyMapper.put("query", Param.SEARCH_TERM); + propertyMapper.put("shipping", Param.SHIPPING); + propertyMapper.put("tax", Param.TAX); + propertyMapper.put("total", Param.VALUE); + propertyMapper.put("revenue", Param.VALUE); + propertyMapper.put("order_id", Param.TRANSACTION_ID); + propertyMapper.put("currency", Param.CURRENCY); + return propertyMapper; + } - Context context = analytics.getApplication(); + private final Logger logger; + private final FirebaseAnalytics firebaseAnalytics; - return new FirebaseIntegration(context); - } + public FirebaseIntegration(Context context, Logger logger) { + this.firebaseAnalytics = FirebaseAnalytics.getInstance(context); + this.logger = logger; + } - @Override public String key() { - return FIREBASE_ANALYTICS_KEY; + @Override + public void onActivityResumed(Activity activity) { + super.onActivityResumed(activity); + + PackageManager packageManager = activity.getPackageManager(); + try { + ActivityInfo info = + packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + String activityLabel = info.loadLabel(packageManager).toString(); + firebaseAnalytics.setCurrentScreen(activity, activityLabel, null); + logger.verbose("firebaseAnalytics.setCurrentScreen(activity, %s, null);", activityLabel); + } catch (PackageManager.NameNotFoundException e) { + throw new AssertionError("Activity Not Found: " + e.toString()); } - }; - - private static final String FIREBASE_ANALYTICS_KEY = "Firebase Analytics"; - - public FirebaseIntegration(Context context) { - mFirebaseAnalytics = FirebaseAnalytics.getInstance(context); } - @Override public void onActivityStarted(Activity activity) { - super.onActivityStarted(activity); + @Override + public void identify(IdentifyPayload identify) { + super.identify(identify); + if (!isNullOrEmpty(identify.userId())) { + firebaseAnalytics.setUserId(identify.userId()); + } + Map traits = identify.traits(); + for (Map.Entry entry : traits.entrySet()) { + String trait = entry.getKey(); + trait = trait.trim().replaceAll(" ", "_"); + if (trait.length() > 40) { + trait = trimKey(trait); + } + String formattedValue; + if (entry.getValue() instanceof Date) { + Date value = (Date) entry.getValue(); + formattedValue = formatDate(value); + } else { + formattedValue = String.valueOf(entry.getValue()); + } + firebaseAnalytics.setUserProperty(trait, formattedValue); + logger.verbose("firebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); + } } - @Override public void onActivityStopped(Activity activity) { - super.onActivityStopped(activity); + @Override + public void track(TrackPayload track) { + super.track(track); + String event = track.event(); + String eventName = mapEvent(event); + Properties properties = track.properties(); + Bundle formattedProperties = formatProperties(properties); + firebaseAnalytics.logEvent(eventName, formattedProperties); + logger.verbose("firebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); } - @Override public void identify(IdentifyPayload identify) { - super.identify(identify); - - if (!isNullOrEmpty(identify.userId())) { - mFirebaseAnalytics.setUserId(identify.userId()); + private String mapEvent(String event) { + String eventName = event; + if (eventMapper.containsKey(eventName)) { + eventName = eventMapper.get(eventName); } + eventName = eventName.trim().replaceAll(" ", "_"); + if (eventName.length() > 40) { + eventName = trimKey(eventName); + } + return eventName; + } - // mFirebaseAnalytics.setUserProperty(key, value); - + private Bundle formatProperties(Properties properties) { + Bundle bundle = new Bundle(); + if ((properties.revenue() != 0 || properties.total() != 0) + && isNullOrEmpty(properties.currency())) { + bundle.putString(Param.CURRENCY, "USD"); + } + for (Map.Entry entry : properties.entrySet()) { + String property = entry.getKey(); + property = mapProperty(property); + if (entry.getValue() instanceof Integer) { + int value = (int) entry.getValue(); + bundle.putInt(property, value); + logger.verbose("bundle.putInt(%s, %s);", property, value); + continue; + } + if (entry.getValue() instanceof Double) { + double value = (double) entry.getValue(); + bundle.putDouble(property, value); + logger.verbose("bundle.putDouble(%s, %s);", property, value); + continue; + } + if (entry.getValue() instanceof Long) { + long value = (long) entry.getValue(); + bundle.putLong(property, value); + logger.verbose("bundle.putLong(%s, %s);", property, value); + continue; + } + if (entry.getValue() instanceof String) { + String value = String.valueOf(entry.getValue()); + bundle.putString(property, value); + logger.verbose("bundle.putString(%s, %s);", property, value); + continue; + } + if (entry.getValue() instanceof Date) { + Date value = (Date) entry.getValue(); + String formattedDate = formatDate(value); + bundle.putString(property, formattedDate); + logger.verbose("bundle.putString(%s, %s);", property, formattedDate); + continue; + } + } + return bundle; } - @Override public void screen(ScreenPayload screen) { - super.screen(screen); -// mFirebaseAnalytics.setCurrentScreen(this, screen.name(), null /* class override */); + private String mapProperty(String property) { + if (propertyMapper.containsKey(property)) { + property = propertyMapper.get(property); + } + property = property.trim().replaceAll(" ", "_"); + if (property.length() > 40) { + property = trimKey(property); + } + return property; } - @Override public void track(TrackPayload track) { - super.track(track); + private String trimKey(String string) { + return string.substring(0, Math.min(string.length(), 40)); + } + private String formatDate(Date date) { + String stringifiedValue = toISO8601Date(date); + String truncatedValue = stringifiedValue.substring(0, 10); + return truncatedValue; } } diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java new file mode 100644 index 0000000..ed09430 --- /dev/null +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -0,0 +1,57 @@ +package com.segment.analytics.android.integration.firebase; + +import static com.segment.analytics.Analytics.LogLevel.VERBOSE; +import static com.segment.analytics.Utils.createTraits; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.content.Context; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.segment.analytics.Traits; +import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; +import com.segment.analytics.core.tests.BuildConfig; +import com.segment.analytics.integrations.Logger; +import com.segment.analytics.test.IdentifyPayloadBuilder; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +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.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class) +@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) +@PrepareForTest(FirebaseAnalytics.class) +public class FirebaseTest { + + @Rule public PowerMockRule rule = new PowerMockRule(); + private FirebaseAnalytics firebase; + private FirebaseIntegration integration; + + @Before + public void setUp() { + firebase = PowerMockito.mock(FirebaseAnalytics.class); + Context context = PowerMockito.mock(Context.class); + + PowerMockito.mockStatic(FirebaseAnalytics.class); + Mockito.when(FirebaseAnalytics.getInstance(context)).thenReturn(firebase); + + integration = new FirebaseIntegration(context, Logger.with(VERBOSE)); + } + + @Test + public void identify() { + Traits traits = createTraits("foo"); + + integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); + + verify(firebase).setUserId("foo"); + } +}