diff --git a/facebook-core/src/main/java/com/facebook/appevents/AppEvent.java b/facebook-core/src/main/java/com/facebook/appevents/AppEvent.java index 88d82316bb..eed4e44bb6 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/AppEvent.java +++ b/facebook-core/src/main/java/com/facebook/appevents/AppEvent.java @@ -52,6 +52,7 @@ class AppEvent implements Serializable { private final JSONObject jsonObject; private final boolean isImplicit; + private final boolean inBackground; private final String name; private final String checksum; @@ -61,6 +62,7 @@ public AppEvent( Double valueToSum, Bundle parameters, boolean isImplicitlyLogged, + boolean isInBackground, @Nullable final UUID currentSessionId ) throws JSONException, FacebookException { jsonObject = getJSONObjectForAppEvent( @@ -69,8 +71,10 @@ public AppEvent( valueToSum, parameters, isImplicitlyLogged, + isInBackground, currentSessionId); isImplicit = isImplicitlyLogged; + inBackground = isInBackground; name = eventName; checksum = calculateChecksum(); } @@ -82,11 +86,13 @@ public String getName() { private AppEvent( String jsonString, boolean isImplicit, + boolean inBackground, String checksum) throws JSONException { jsonObject = new JSONObject(jsonString); this.isImplicit = isImplicit; this.name = jsonObject.optString(Constants.EVENT_NAME_EVENT_KEY); this.checksum = checksum; + this.inBackground = inBackground; } public boolean getIsImplicit() { @@ -158,6 +164,7 @@ private static JSONObject getJSONObjectForAppEvent( Double valueToSum, Bundle parameters, boolean isImplicitlyLogged, + boolean isInBackground, @Nullable final UUID currentSessionId ) throws FacebookException, JSONException{ validateIdentifier(eventName); @@ -180,6 +187,10 @@ private static JSONObject getJSONObjectForAppEvent( eventObject.put("_implicitlyLogged", "1"); } + if (isInBackground) { + eventObject.put("_inBackground", "1"); + } + if (parameters != null) { for (String key : parameters.keySet()) { @@ -213,14 +224,16 @@ static class SerializationProxyV1 implements Serializable { private static final long serialVersionUID = -2488473066578201069L; private final String jsonString; private final boolean isImplicit; + private final boolean inBackground; - private SerializationProxyV1(String jsonString, boolean isImplicit) { + private SerializationProxyV1(String jsonString, boolean isImplicit, boolean inBackground) { this.jsonString = jsonString; this.isImplicit = isImplicit; + this.inBackground = inBackground; } private Object readResolve() throws JSONException { - return new AppEvent(jsonString, isImplicit, null); + return new AppEvent(jsonString, isImplicit, inBackground, null); } } @@ -228,21 +241,27 @@ static class SerializationProxyV2 implements Serializable { private static final long serialVersionUID = 2016_08_03_001L; private final String jsonString; private final boolean isImplicit; + private final boolean inBackground; private final String checksum; - private SerializationProxyV2(String jsonString, boolean isImplicit, String checksum) { + private SerializationProxyV2( + String jsonString, + boolean isImplicit, + boolean inBackground, + String checksum) { this.jsonString = jsonString; this.isImplicit = isImplicit; + this.inBackground = inBackground; this.checksum = checksum; } private Object readResolve() throws JSONException { - return new AppEvent(jsonString, isImplicit, checksum); + return new AppEvent(jsonString, isImplicit, inBackground, checksum); } } private Object writeReplace() { - return new SerializationProxyV2(jsonObject.toString(), isImplicit, checksum); + return new SerializationProxyV2(jsonObject.toString(), isImplicit, inBackground, checksum); } @Override diff --git a/facebook-core/src/main/java/com/facebook/appevents/AppEventsLogger.java b/facebook-core/src/main/java/com/facebook/appevents/AppEventsLogger.java index 2feaf62b07..1c74a433a5 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/AppEventsLogger.java +++ b/facebook-core/src/main/java/com/facebook/appevents/AppEventsLogger.java @@ -1302,6 +1302,7 @@ private void logEvent( valueToSum, parameters, isImplicitlyLogged, + ActivityLifecycleTracker.isInBackground(), currentSessionId); logEvent(event, accessTokenAppId); } catch (JSONException jsonException) { diff --git a/facebook-core/src/main/java/com/facebook/appevents/internal/ActivityLifecycleTracker.java b/facebook-core/src/main/java/com/facebook/appevents/internal/ActivityLifecycleTracker.java index 5052fc7175..ac262a00f6 100644 --- a/facebook-core/src/main/java/com/facebook/appevents/internal/ActivityLifecycleTracker.java +++ b/facebook-core/src/main/java/com/facebook/appevents/internal/ActivityLifecycleTracker.java @@ -28,6 +28,7 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; import android.util.Log; import com.facebook.FacebookSdk; @@ -82,6 +83,7 @@ public class ActivityLifecycleTracker { private static String deviceSessionID = null; private static Boolean isAppIndexingEnabled = false; private static volatile Boolean isCheckingSession = false; + private static int activityReferences = 0; public static void startTracking(Application application, final String appId) { if (!tracking.compareAndSet(false, true)) { @@ -103,6 +105,7 @@ public void onActivityCreated( @Override public void onActivityStarted(Activity activity) { + ActivityLifecycleTracker.activityReferences++; Logger.log(LoggingBehavior.APP_EVENTS, TAG, "onActivityStarted"); } @@ -124,6 +127,7 @@ public void onActivityPaused(final Activity activity) { public void onActivityStopped(Activity activity) { Logger.log(LoggingBehavior.APP_EVENTS, TAG, "onActivityStopped"); AppEventsLogger.onContextStop(); + ActivityLifecycleTracker.activityReferences--; } @Override @@ -138,6 +142,11 @@ public void onActivityDestroyed(Activity activity) { }); } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static boolean isInBackground() { + return 0 == activityReferences; + } + public static boolean isTracking() { return tracking.get(); } diff --git a/facebook/src/test/java/com/facebook/GraphRequestTest.java b/facebook/src/test/java/com/facebook/GraphRequestTest.java index 6346907de0..b6114f24ba 100644 --- a/facebook/src/test/java/com/facebook/GraphRequestTest.java +++ b/facebook/src/test/java/com/facebook/GraphRequestTest.java @@ -20,11 +20,13 @@ package com.facebook; +import android.content.Context; import android.graphics.Bitmap; import android.location.Location; import android.net.Uri; import android.os.Bundle; +import com.facebook.internal.AttributionIdentifiers; import com.facebook.internal.Utility; import com.facebook.share.internal.ShareInternalUtility; @@ -32,11 +34,13 @@ import org.junit.Before; import org.junit.Test; import org.powermock.core.classloader.annotations.PrepareForTest; +import org.robolectric.RuntimeEnvironment; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.List; +import static com.facebook.TestUtils.assertEqualContentsWithoutOrder; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.times; @@ -47,7 +51,14 @@ import static org.powermock.api.mockito.PowerMockito.spy; import static org.powermock.api.mockito.PowerMockito.when; -@PrepareForTest({FacebookSdk.class, AccessTokenManager.class, GraphResponse.class, Utility.class}) +@PrepareForTest({ + AccessToken.class, + AccessTokenManager.class, + AttributionIdentifiers.class, + FacebookSdk.class, + GraphResponse.class, + Utility.class +}) public class GraphRequestTest extends FacebookPowerMockTestCase { @Before @@ -83,7 +94,7 @@ public void testCreatePostRequest() { assertNull(request2.getAccessToken()); assertEquals(HttpMethod.POST, request2.getHttpMethod()); assertEquals(graphPath, request2.getGraphPath()); - assertEquals(parameters, request2.getParameters()); + assertEqualContentsWithoutOrder(parameters, request2.getParameters()); assertNull(request2.getCallback()); } @@ -265,4 +276,27 @@ public void testCallback() throws Exception { verify(callback, times(1)).onCompleted(any(GraphResponse.class)); } + + @Test + public void testRequestForCustomAudienceThirdPartyID() throws Exception { + mockStatic(AttributionIdentifiers.class); + when(AttributionIdentifiers.getAttributionIdentifiers(any(Context.class))).thenReturn(null); + doReturn(false).when(FacebookSdk.class, "getLimitEventAndDataUsage", any(Context.class)); + GraphRequest expectedRequest = new GraphRequest( + null, + "mockAppID/custom_audience_third_party_id", + new Bundle(), + HttpMethod.GET, + null); + + GraphRequest request = GraphRequest.newCustomAudienceThirdPartyIdRequest( + mock(AccessToken.class), + RuntimeEnvironment.application, + "mockAppID", + null); + + assertEquals(expectedRequest.getGraphPath(), request.getGraphPath()); + assertEquals(expectedRequest.getHttpMethod(), request.getHttpMethod()); + assertEqualContentsWithoutOrder(expectedRequest.getParameters(), request.getParameters()); + } } diff --git a/facebook/src/test/java/com/facebook/appevents/AppEventTestUtilities.java b/facebook/src/test/java/com/facebook/appevents/AppEventTestUtilities.java index 8272100bf9..2917f5c2dc 100644 --- a/facebook/src/test/java/com/facebook/appevents/AppEventTestUtilities.java +++ b/facebook/src/test/java/com/facebook/appevents/AppEventTestUtilities.java @@ -26,12 +26,14 @@ import org.json.JSONException; import org.json.JSONObject; +import org.mockito.ArgumentMatcher; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.UUID; +import static com.facebook.TestUtils.assertEqualContentsWithoutOrder; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -46,35 +48,26 @@ public static AppEvent getTestAppEvent() throws Exception { 1.0, customParams, false, + false, UUID.fromString("65565271-1ace-4580-bd13-b2bc6d0df035")); appEvent.isChecksumValid(); return appEvent; } - public static void assertEquals(JSONObject expected, JSONObject actual) throws JSONException { - if (expected == null) { - assertNull(actual); - } - assertNotNull(actual); + public static class BundleMatcher extends ArgumentMatcher { - Set set1 = getKeySet(expected); - Set set2 = getKeySet(actual); - Assert.assertEquals(set1, set2); + private Bundle wanted; - for (String k : set1) { - Assert.assertEquals(expected.get(k), actual.get(k)); + public BundleMatcher(Bundle wanted) { + this.wanted = wanted; } - } - public static Set getKeySet(JSONObject object){ - Set set = new HashSet<>(); - - Iterator keysItr = object.keys(); - while(keysItr.hasNext()) { - String key = keysItr.next(); - set.add(key); + public boolean matches(Object bundle) { + if (!(bundle instanceof Bundle)) { + return false; + } + assertEqualContentsWithoutOrder(this.wanted, (Bundle)bundle); + return true; } - return set; } - } diff --git a/facebook/src/test/java/com/facebook/appevents/AppEventsLoggerTest.java b/facebook/src/test/java/com/facebook/appevents/AppEventsLoggerTest.java index 61e5558646..6f465f1bcf 100644 --- a/facebook/src/test/java/com/facebook/appevents/AppEventsLoggerTest.java +++ b/facebook/src/test/java/com/facebook/appevents/AppEventsLoggerTest.java @@ -20,20 +20,26 @@ package com.facebook.appevents; +import android.content.Context; import android.os.Bundle; import com.facebook.FacebookPowerMockTestCase; import com.facebook.FacebookSdk; +import com.facebook.GraphRequest; +import com.facebook.HttpMethod; +import com.facebook.TestUtils; import com.facebook.appevents.internal.ActivityLifecycleTracker; import com.facebook.appevents.internal.AppEventUtility; import com.facebook.appevents.internal.AppEventsLoggerUtility; import com.facebook.appevents.internal.Constants; +import com.facebook.internal.AttributionIdentifiers; import com.facebook.internal.Utility; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -48,6 +54,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.powermock.api.mockito.PowerMockito.doReturn; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; @@ -61,7 +72,10 @@ AppEventQueue.class, AppEventUtility.class, AppEventsLogger.class, + AppEventsLogger.class, + AttributionIdentifiers.class, FacebookSdk.class, + GraphRequest.class, Utility.class, }) public class AppEventsLoggerTest extends FacebookPowerMockTestCase { @@ -99,7 +113,7 @@ public void testSetAndClearUserData() throws JSONException { "em@gmail.com", "fn", "ln", "123", null, null, null, null, null, null); JSONObject actualUserData = new JSONObject(AppEventsLogger.getUserData()); JSONObject expectedUserData = new JSONObject("{\"ln\":\"e545c2c24e6463d7c4fe3829940627b226c0b9be7a8c7dbe964768da48f1ab9d\",\"ph\":\"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\",\"em\":\"5f341666fb1ce60d716e4afc302c8658f09412290aa2ca8bc623861f452f9d33\",\"fn\":\"0f1e18bb4143dc4be22e61ea4deb0491c2bf7018c6504ad631038aed5ca4a0ca\"}"); - AppEventTestUtilities.assertEquals(expectedUserData, actualUserData); + TestUtils.assertEquals(expectedUserData, actualUserData); AppEventsLogger.clearUserData(); assertTrue(AppEventsLogger.getUserData().isEmpty()); @@ -114,6 +128,15 @@ public void testSetAndClearUserID() { assertNull(AppEventsLogger.getUserID()); } + @Test + public void testSetFlushBehavior() { + AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.AUTO); + assertEquals(AppEventsLogger.FlushBehavior.AUTO, AppEventsLogger.getFlushBehavior()); + + AppEventsLogger.setFlushBehavior(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY); + assertEquals(AppEventsLogger.FlushBehavior.EXPLICIT_ONLY, AppEventsLogger.getFlushBehavior()); + } + @Test public void testUserIDAddedToAppEvent() throws Exception { String userID = "12345678"; @@ -143,6 +166,7 @@ public void testLogEvent() throws Exception { Matchers.anyDouble(), Matchers.any(Bundle.class), Matchers.anyBoolean(), + Matchers.anyBoolean(), Matchers.any(UUID.class)); } @@ -154,17 +178,19 @@ public void testLogPurchase() throws Exception { parameters.putString( AppEventsConstants.EVENT_PARAM_CURRENCY, Currency.getInstance(Locale.US).getCurrencyCode()); + verifyNew(AppEvent.class).withArguments( Matchers.anyString(), Matchers.eq(AppEventsConstants.EVENT_NAME_PURCHASED), Matchers.eq(1.0), - Matchers.eq(parameters), + argThat(new AppEventTestUtilities.BundleMatcher(parameters)), + Matchers.anyBoolean(), Matchers.anyBoolean(), Matchers.any(UUID.class)); } @Test - public void testLogProductItem() throws Exception { + public void testLogProductItemWithGtinMpnBrand() throws Exception { AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logProductItem( "F40CEE4E-471E-45DB-8541-1526043F4B21", AppEventsLogger.ProductAvailability.IN_STOCK, @@ -206,7 +232,54 @@ public void testLogProductItem() throws Exception { Matchers.anyString(), Matchers.eq(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE), Matchers.anyDouble(), - Matchers.eq(parameters), + argThat(new AppEventTestUtilities.BundleMatcher(parameters)), + Matchers.anyBoolean(), + Matchers.anyBoolean(), + Matchers.any(UUID.class)); + } + + @Test + public void testLogProductItemWithoutGtinMpnBrand() throws Exception { + AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logProductItem( + "F40CEE4E-471E-45DB-8541-1526043F4B21", + AppEventsLogger.ProductAvailability.IN_STOCK, + AppEventsLogger.ProductCondition.NEW, + "description", + "https://www.sample.com", + "https://www.sample.com", + "title", + new BigDecimal(1.0), + Currency.getInstance(Locale.US), + null, + null, + null, + null); + Bundle parameters = new Bundle(); + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_ITEM_ID, + "F40CEE4E-471E-45DB-8541-1526043F4B21"); + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_AVAILABILITY, + AppEventsLogger.ProductAvailability.IN_STOCK.name()); + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_CONDITION, + AppEventsLogger.ProductCondition.NEW.name()); + parameters.putString(Constants.EVENT_PARAM_PRODUCT_DESCRIPTION, "description"); + parameters.putString(Constants.EVENT_PARAM_PRODUCT_IMAGE_LINK, "https://www.sample.com"); + parameters.putString(Constants.EVENT_PARAM_PRODUCT_LINK, "https://www.sample.com"); + parameters.putString(Constants.EVENT_PARAM_PRODUCT_TITLE, "title"); + parameters.putString(Constants.EVENT_PARAM_PRODUCT_PRICE_AMOUNT, + (new BigDecimal(1.0)).setScale(3, BigDecimal.ROUND_HALF_UP).toString()); + parameters.putString( + Constants.EVENT_PARAM_PRODUCT_PRICE_CURRENCY, + Currency.getInstance(Locale.US).getCurrencyCode()); + + verifyNew(AppEvent.class, never()).withArguments( + Matchers.anyString(), + Matchers.eq(AppEventsConstants.EVENT_NAME_PRODUCT_CATALOG_UPDATE), + Matchers.anyDouble(), + argThat(new AppEventTestUtilities.BundleMatcher(parameters)), + Matchers.anyBoolean(), Matchers.anyBoolean(), Matchers.any(UUID.class)); } @@ -214,16 +287,68 @@ public void testLogProductItem() throws Exception { @Test public void testLogPushNotificationOpen() throws Exception { Bundle payload = new Bundle(); - payload.putString("fb_push_payload", "{\"campaign\" : \"test\"}"); + payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}"); AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logPushNotificationOpen(payload); Bundle parameters = new Bundle(); - parameters.putString("fb_push_campaign", "test"); + parameters.putString("fb_push_campaign", "testCampaign"); + + verifyNew(AppEvent.class).withArguments( + Matchers.anyString(), + Matchers.eq("fb_mobile_push_opened"), + Matchers.anyDouble(), + argThat(new AppEventTestUtilities.BundleMatcher(parameters)), + Matchers.anyBoolean(), + Matchers.anyBoolean(), + Matchers.any(UUID.class)); + } + + @Test + public void testLogPushNotificationOpenWithoutCampaign() throws Exception { + Bundle payload = new Bundle(); + payload.putString("fb_push_payload", "{}"); + AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logPushNotificationOpen(payload); + + verifyNew(AppEvent.class, never()).withArguments( + Matchers.anyString(), + Matchers.anyString(), + Matchers.anyDouble(), + Matchers.any(Bundle.class), + Matchers.anyBoolean(), + Matchers.anyBoolean(), + Matchers.any(UUID.class)); + } + + @Test + public void testLogPushNotificationOpenWithAction() throws Exception { + Bundle payload = new Bundle(); + payload.putString("fb_push_payload", "{\"campaign\" : \"testCampaign\"}"); + AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logPushNotificationOpen(payload, "testAction"); + Bundle parameters = new Bundle(); + parameters.putString("fb_push_campaign", "testCampaign"); + parameters.putString("fb_push_action", "testAction"); verifyNew(AppEvent.class).withArguments( Matchers.anyString(), Matchers.eq("fb_mobile_push_opened"), Matchers.anyDouble(), - Matchers.eq(parameters), + argThat(new AppEventTestUtilities.BundleMatcher(parameters)), + Matchers.anyBoolean(), + Matchers.anyBoolean(), + Matchers.any(UUID.class)); + } + + @Test + public void testLogPushNotificationOpenWithoutPayload() throws Exception { + when(Utility.isNullOrEmpty(anyString())).thenReturn(true); + Bundle payload = new Bundle(); + AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()).logPushNotificationOpen(payload); + + verifyNew(AppEvent.class, never()).withArguments( + Matchers.anyString(), + Matchers.anyString(), + Matchers.anyDouble(), + Matchers.any(Bundle.class), + Matchers.anyBoolean(), Matchers.anyBoolean(), Matchers.any(UUID.class)); } @@ -232,13 +357,39 @@ public void testLogPushNotificationOpen() throws Exception { public void testSetPushNotificationsRegistrationId() throws Exception { String mockNotificationId = "123"; AppEventsLogger.setPushNotificationsRegistrationId(mockNotificationId); + verifyNew(AppEvent.class).withArguments( Matchers.anyString(), Matchers.eq(AppEventsConstants.EVENT_NAME_PUSH_TOKEN_OBTAINED), Matchers.anyDouble(), Matchers.any(Bundle.class), Matchers.anyBoolean(), + Matchers.anyBoolean(), Matchers.any(UUID.class)); assertEquals(mockNotificationId, AppEventsLogger.getPushNotificationsRegistrationId()); } + + @Test + public void testPublishInstall() throws Exception { + GraphRequest mockRequest = mock(GraphRequest.class); + PowerMockito.whenNew(GraphRequest.class).withAnyArguments().thenReturn(mockRequest); + mockStatic(AttributionIdentifiers.class); + when(AttributionIdentifiers.getAttributionIdentifiers(any(Context.class))).thenReturn(null); + String expectedEvent = "MOBILE_APP_INSTALL"; + String expectedUrl = "mockAppID/activities"; + final ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); + + FacebookSdk.publishInstallAsync(FacebookSdk.getApplicationContext(), "mockAppID"); + + verifyNew(GraphRequest.class).withArguments( + Matchers.isNull(), + Matchers.eq(expectedUrl), + Matchers.isNull(), + Matchers.eq(HttpMethod.POST), + Matchers.isNull() + ); + verify(mockRequest).setGraphObject(captor.capture()); + assertEquals(expectedEvent, captor.getValue().getString("event")); + } + }