diff --git a/Adjust/build.gradle b/Adjust/build.gradle index b2fc5a8c2..1864f6d42 100644 --- a/Adjust/build.gradle +++ b/Adjust/build.gradle @@ -24,7 +24,7 @@ android { compileSdkVersion 19 defaultConfig { versionCode 11 - versionName '3.2.0' + versionName '3.3.0' minSdkVersion 8 targetSdkVersion 19 } diff --git a/Adjust/pom.xml b/Adjust/pom.xml index 2526a7254..fcc97f2d0 100644 --- a/Adjust/pom.xml +++ b/Adjust/pom.xml @@ -5,7 +5,7 @@ 4.0.0 adjust-android com.adjust.sdk - 3.2.0 + 3.3.0 jar UTF-8 diff --git a/Adjust/src/com/adjust/sdk/ActivityHandler.java b/Adjust/src/com/adjust/sdk/ActivityHandler.java index d71a6e8aa..a57144a77 100644 --- a/Adjust/src/com/adjust/sdk/ActivityHandler.java +++ b/Adjust/src/com/adjust/sdk/ActivityHandler.java @@ -24,6 +24,7 @@ import java.io.ObjectOutputStream; import java.io.OptionalDataException; import java.lang.ref.WeakReference; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.Executors; @@ -36,6 +37,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -49,6 +51,7 @@ public class ActivityHandler extends HandlerThread { private static long SESSION_INTERVAL; private static long SUBSESSION_INTERVAL; private static final String TIME_TRAVEL = "Time travel!"; + private static final String ADJUST_PREFIX = "adjust_"; private final SessionHandler sessionHandler; private IPackageHandler packageHandler; @@ -195,6 +198,13 @@ public Boolean isEnabled() { } } + public void readOpenUrl(Uri url) { + Message message = Message.obtain(); + message.arg1 = SessionHandler.DEEP_LINK; + message.obj = url; + sessionHandler.sendMessage(message); + } + private static final class SessionHandler extends Handler { private static final int INIT_BUNDLE = 72630; private static final int INIT_PRESET = 72633; @@ -202,6 +212,8 @@ private static final class SessionHandler extends Handler { private static final int END = 72650; private static final int EVENT = 72660; private static final int REVENUE = 72670; + private static final int DEEP_LINK = 72680; + private final WeakReference sessionHandlerReference; @@ -240,6 +252,10 @@ public void handleMessage(Message message) { PackageBuilder revenueBuilder = (PackageBuilder) message.obj; sessionHandler.trackRevenueInternal(revenueBuilder); break; + case DEEP_LINK: + Uri url = (Uri) message.obj; + sessionHandler.readOpenUrlInternal(url); + break; } } } @@ -412,6 +428,49 @@ private void trackRevenueInternal(PackageBuilder revenueBuilder) { logger.debug(String.format(Locale.US, "Event %d (revenue)", activityState.eventCount)); } + private void readOpenUrlInternal(Uri url) { + if (url == null) { + return; + } + + String queryString = url.getQuery(); + if (queryString == null) { + return; + } + + Map adjustDeepLinks = new HashMap(); + + String[] queryPairs = queryString.split("&"); + for (String pair : queryPairs) { + String[] pairComponents = pair.split("="); + if (pairComponents.length != 2) continue; + + String key = pairComponents[0]; + if (!key.startsWith(ADJUST_PREFIX)) continue; + + String value = pairComponents[1]; + if (value.length() == 0) continue; + + String keyWOutPrefix = key.substring(ADJUST_PREFIX.length()); + if (keyWOutPrefix.length() == 0) continue; + + adjustDeepLinks.put(keyWOutPrefix, value); + } + + if (adjustDeepLinks.size() == 0) { + return; + } + + PackageBuilder builder = new PackageBuilder(context); + builder.setDeepLinkParameters(adjustDeepLinks); + injectGeneralAttributes(builder); + ActivityPackage reattributionPackage = builder.buildReattributionPackage(); + packageHandler.addPackage(reattributionPackage); + packageHandler.sendFirstPackage(); + + logger.debug(String.format("Reattribution %s", adjustDeepLinks.toString())); + } + private boolean canTrackEvent(PackageBuilder revenueBuilder) { return checkAppTokenNotNull(appToken) && checkActivityState(activityState) @@ -486,7 +545,7 @@ private void writeActivityState() { try { objectStream.writeObject(activityState); - logger.verbose(String.format("Wrote activity state: %s", activityState)); + logger.debug(String.format("Wrote activity state: %s", activityState)); } catch (NotSerializableException e) { logger.error("Failed to serialize activity state"); } finally { diff --git a/Adjust/src/com/adjust/sdk/ActivityKind.java b/Adjust/src/com/adjust/sdk/ActivityKind.java index 3d57697ce..32942b616 100644 --- a/Adjust/src/com/adjust/sdk/ActivityKind.java +++ b/Adjust/src/com/adjust/sdk/ActivityKind.java @@ -1,7 +1,7 @@ package com.adjust.sdk; public enum ActivityKind { - UNKNOWN, SESSION, EVENT, REVENUE; + UNKNOWN, SESSION, EVENT, REVENUE, REATTRIBUTION; public static ActivityKind fromString(String string) { if ("session".equals(string)) { @@ -10,17 +10,21 @@ public static ActivityKind fromString(String string) { return EVENT; } else if ("revenue".equals(string)) { return REVENUE; + } else if ("reattribution".equals(string)) { + return REATTRIBUTION; } else { return UNKNOWN; } } + @Override public String toString() { switch(this) { - case SESSION: return "session"; - case EVENT: return "event"; - case REVENUE: return "revenue"; - default: return "unknown"; + case SESSION: return "session"; + case EVENT: return "event"; + case REVENUE: return "revenue"; + case REATTRIBUTION: return "reattribution"; + default: return "unknown"; } } } diff --git a/Adjust/src/com/adjust/sdk/ActivityState.java b/Adjust/src/com/adjust/sdk/ActivityState.java index ec2ab40d5..01c212b7f 100644 --- a/Adjust/src/com/adjust/sdk/ActivityState.java +++ b/Adjust/src/com/adjust/sdk/ActivityState.java @@ -93,8 +93,20 @@ private void readObject(ObjectInputStream stream) throws NotActiveException, IOE lastActivity = fields.get("lastActivity", -1l); createdAt = fields.get("createdAt", -1l); lastInterval = fields.get("lastInterval", -1l); - uuid = (String)fields.get("uuid", null); - enabled = fields.get("enabled", true); + + // default values for migrating devices + uuid = null; + enabled = true; + // try to read in order of less recent new fields + try { + uuid = (String)fields.get("uuid", null); + enabled = fields.get("enabled", true); + // add new fields here + } catch (Exception e) { + Logger logger = AdjustFactory.getLogger(); + logger.debug(String.format("Unable to read new field in migration device with error (%s)", + e.getMessage())); + } // create UUID for migrating devices if (uuid == null) { diff --git a/Adjust/src/com/adjust/sdk/Adjust.java b/Adjust/src/com/adjust/sdk/Adjust.java index bee0ec505..eea8bb223 100644 --- a/Adjust/src/com/adjust/sdk/Adjust.java +++ b/Adjust/src/com/adjust/sdk/Adjust.java @@ -14,6 +14,7 @@ import java.util.Map; import android.app.Activity; +import android.net.Uri; /** * The main interface to Adjust. @@ -148,6 +149,16 @@ public static Boolean isEnabled() { return false; } + public static void appWillOpenUrl(Uri url) { + try { + activityHandler.readOpenUrl(url); + } catch (NullPointerException e) { + if (logger != null) + logger.error(NO_ACTIVITY_HANDLER_FOUND); + } + + } + // Special appDidLaunch method used by SDK wrappers such as our Adobe Air SDK. protected static void appDidLaunch(Activity activity, String appToken, String environment, String logLevel, boolean eventBuffering) { diff --git a/Adjust/src/com/adjust/sdk/Constants.java b/Adjust/src/com/adjust/sdk/Constants.java index fdee2ec34..b1452e100 100644 --- a/Adjust/src/com/adjust/sdk/Constants.java +++ b/Adjust/src/com/adjust/sdk/Constants.java @@ -19,7 +19,7 @@ public interface Constants { int THIRTY_MINUTES = 30 * ONE_MINUTE; String BASE_URL = "https://app.adjust.io"; - String CLIENT_SDK = "android3.2.0"; + String CLIENT_SDK = "android3.3.0"; String LOGTAG = "Adjust"; String SESSION_STATE_FILENAME = "AdjustIoActivityState"; diff --git a/Adjust/src/com/adjust/sdk/PackageBuilder.java b/Adjust/src/com/adjust/sdk/PackageBuilder.java index 0f3c1db35..3445a1566 100644 --- a/Adjust/src/com/adjust/sdk/PackageBuilder.java +++ b/Adjust/src/com/adjust/sdk/PackageBuilder.java @@ -21,7 +21,7 @@ public class PackageBuilder { - private Context context; + private Context context; // general private String appToken; @@ -50,6 +50,9 @@ public class PackageBuilder { private double amountInCents; private Map callbackParameters; + // reattributions + private Map deepLinkParameters; + public PackageBuilder(Context context) { this.context = context; @@ -147,6 +150,10 @@ public void setCallbackParameters(Map callbackParameters) { this.callbackParameters = callbackParameters; } + public void setDeepLinkParameters(Map deepLinkParameters) { + this.deepLinkParameters = deepLinkParameters; + } + public boolean isValidForEvent() { if (null == eventToken) { Logger logger = AdjustFactory.getLogger(); @@ -210,6 +217,19 @@ public ActivityPackage buildRevenuePackage() { return revenuePackage; } + public ActivityPackage buildReattributionPackage() { + Map parameters = getDefaultParameters(); + addMapJson(parameters, "deeplink_parameters", deepLinkParameters); + + ActivityPackage reattributionPackage = getDefaultActivityPackage(); + reattributionPackage.setPath("/reattribute"); + reattributionPackage.setActivityKind(ActivityKind.REATTRIBUTION); + reattributionPackage.setSuffix(""); + reattributionPackage.setParameters(parameters); + + return reattributionPackage; + } + private boolean isEventTokenValid() { if (6 != eventToken.length()) { Logger logger = AdjustFactory.getLogger(); @@ -253,7 +273,7 @@ private Map getDefaultParameters() { private void injectEventParameters(Map parameters) { addInt(parameters, "event_count", eventCount); addString(parameters, "event_token", eventToken); - addMap(parameters, "params", callbackParameters); + addMapBase64(parameters, "params", callbackParameters); } private String getAmountString() { @@ -309,7 +329,7 @@ private void addDuration(Map parameters, String key, long durati addInt(parameters, key, durationInSeconds); } - private void addMap(Map parameters, String key, Map map) { + private void addMapBase64(Map parameters, String key, Map map) { if (null == map) { return; } @@ -320,4 +340,15 @@ private void addMap(Map parameters, String key, Map parameters, String key, Map map) { + if (null == map) { + return; + } + + JSONObject jsonObject = new JSONObject(map); + String jsonString = jsonObject.toString(); + + addString(parameters, key, jsonString); + } } diff --git a/Adjust/src/com/adjust/sdk/Util.java b/Adjust/src/com/adjust/sdk/Util.java index 06f1eb262..64b8daf96 100644 --- a/Adjust/src/com/adjust/sdk/Util.java +++ b/Adjust/src/com/adjust/sdk/Util.java @@ -67,19 +67,19 @@ protected static String getUserAgent(final Context context) { final int screenLayout = configuration.screenLayout; final String[] parts = { - getPackageName(context), - getAppVersion(context), - getDeviceType(screenLayout), - getDeviceName(), - getOsName(), - getOsVersion(), - getLanguage(locale), - getCountry(locale), - getScreenSize(screenLayout), - getScreenFormat(screenLayout), - getScreenDensity(displayMetrics), - getDisplayWidth(displayMetrics), - getDisplayHeight(displayMetrics) + getPackageName(context), + getAppVersion(context), + getDeviceType(screenLayout), + getDeviceName(), + getOsName(), + getOsVersion(), + getLanguage(locale), + getCountry(locale), + getScreenSize(screenLayout), + getScreenFormat(screenLayout), + getScreenDensity(displayMetrics), + getDisplayWidth(displayMetrics), + getDisplayHeight(displayMetrics) }; return TextUtils.join(" ", parts); } @@ -351,18 +351,18 @@ public static String dateFormat(long date) { return dateFormat.format(date); } - public static String getGpsAdid(Context context) { - String gpsAdid = null; - try { - AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(context); - if (!info.isLimitAdTrackingEnabled()) { + public static String getGpsAdid(Context context) { + String gpsAdid = null; + try { + AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(context); + if (!info.isLimitAdTrackingEnabled()) { gpsAdid = info.getId(); - } - } catch (Exception e) { + } + } catch (Exception e) { Logger logger = AdjustFactory.getLogger(); - logger.error(String.format("Error getting Google Play Services advertising ID, (%s)", e.getMessage())); - } + logger.error(String.format("Error getting Google Play Services advertising ID, (%s)", e.getMessage())); + } - return gpsAdid; - } + return gpsAdid; + } } diff --git a/Adjust/test/src/com/adjust/sdk/test/TestActivityHandler.java b/Adjust/test/src/com/adjust/sdk/test/TestActivityHandler.java index 8b1fa15c9..22bbe28db 100644 --- a/Adjust/test/src/com/adjust/sdk/test/TestActivityHandler.java +++ b/Adjust/test/src/com/adjust/sdk/test/TestActivityHandler.java @@ -4,10 +4,12 @@ import java.util.Map; import android.content.Context; +import android.net.Uri; import android.os.SystemClock; import android.test.ActivityInstrumentationTestCase2; import com.adjust.sdk.ActivityHandler; +import com.adjust.sdk.ActivityKind; import com.adjust.sdk.ActivityPackage; import com.adjust.sdk.AdjustFactory; import com.adjust.sdk.Logger.LogLevel; @@ -90,7 +92,7 @@ public void testFirstSession() { // check the Sdk version is being tested assertEquals(activityPackage.getExtendedString(), - "android3.2.0", activityPackage.getClientSdk()); + "android3.3.0", activityPackage.getClientSdk()); Map parameters = activityPackage.getParameters(); @@ -125,7 +127,7 @@ public void testFirstSession() { // check that the activity state is written by the first session or timer assertTrue(mockLogger.toString(), - mockLogger.containsMessage(LogLevel.VERBOSE, "Wrote activity state")); + mockLogger.containsMessage(LogLevel.DEBUG, "Wrote activity state")); // ending of first session assertTrue(mockLogger.toString(), @@ -251,7 +253,7 @@ public void testEventsBuffered() { // check the event count in the written activity state assertTrue(mockLogger.toString(), - mockLogger.containsMessage(LogLevel.VERBOSE, "Wrote activity state: ec:1")); + mockLogger.containsMessage(LogLevel.DEBUG, "Wrote activity state: ec:1")); // check the event count in the logger assertTrue(mockLogger.toString(), @@ -293,7 +295,7 @@ public void testEventsBuffered() { // check the event count in the written activity state assertTrue(mockLogger.toString(), - mockLogger.containsMessage(LogLevel.VERBOSE, "Wrote activity state: ec:2")); + mockLogger.containsMessage(LogLevel.DEBUG, "Wrote activity state: ec:2")); // check the event count in the logger assertTrue(mockLogger.toString(), @@ -355,7 +357,7 @@ public void testEventsNotBuffered() { // check the event count in the written activity state assertTrue(mockLogger.toString(), - mockLogger.containsMessage(LogLevel.VERBOSE, "Wrote activity state: ec:1")); + mockLogger.containsMessage(LogLevel.DEBUG, "Wrote activity state: ec:1")); // check the event count in the logger assertTrue(mockLogger.toString(), @@ -397,7 +399,7 @@ public void testEventsNotBuffered() { // check the event count in the written activity state assertTrue(mockLogger.toString(), - mockLogger.containsMessage(LogLevel.VERBOSE, "Wrote activity state: ec:2")); + mockLogger.containsMessage(LogLevel.DEBUG, "Wrote activity state: ec:2")); // check the event count in the logger assertTrue(mockLogger.toString(), @@ -570,4 +572,78 @@ public void testDisable() { mockLogger.containsTestMessage("PackageHandler resumeSending")); // */ } + + public void testOpenUrl() { + Context context = activity.getApplicationContext(); + + // starting from a clean slate + mockLogger.test("Was AdjustActivityState deleted? " + ActivityHandler.deleteActivityState(context)); + + ActivityHandler activityHandler = new ActivityHandler(activity); + activityHandler.trackSubsessionStart(); + + Uri normal = Uri.parse("AdjustTests://example.com/path/inApp?adjust_foo=bar&other=stuff&adjust_key=value"); + Uri emptyQueryString = Uri.parse("AdjustTests://"); + Uri emptyString = Uri.parse(""); + Uri nullString = null; + Uri single = Uri.parse("AdjustTests://example.com/path/inApp?adjust_foo"); + Uri prefix = Uri.parse("AdjustTests://example.com/path/inApp?adjust_=bar"); + Uri incomplete = Uri.parse("AdjustTests://example.com/path/inApp?adjust_foo="); + + activityHandler.readOpenUrl(normal); + activityHandler.readOpenUrl(emptyQueryString); + activityHandler.readOpenUrl(emptyString); + activityHandler.readOpenUrl(nullString); + activityHandler.readOpenUrl(single); + activityHandler.readOpenUrl(prefix); + activityHandler.readOpenUrl(incomplete); + + SystemClock.sleep(1000); + + // check that all supposed packages were sent + // 1 session + 1 reattributions + assertEquals(2, mockPackageHandler.queue.size()); + + // check that the normal url was parsed and sent + ActivityPackage activityPackage = mockPackageHandler.queue.get(1); + + // testing the activity kind is the correct one + ActivityKind activityKind = activityPackage.getActivityKind(); + assertEquals(activityPackage.getExtendedString(), + ActivityKind.REATTRIBUTION, activityKind); + + // testing the conversion from activity kind to string + String activityKindString = activityKind.toString(); + assertEquals(activityPackage.getExtendedString(), + "reattribution", activityKindString); + + // testing the conversion from string to activity kind + activityKind = ActivityKind.fromString(activityKindString); + assertEquals(activityPackage.getExtendedString(), + ActivityKind.REATTRIBUTION, activityKind); + + // package type should be reattribute + assertEquals(activityPackage.getExtendedString(), + "/reattribute", activityPackage.getPath()); + + // suffix should be empty + assertEquals(activityPackage.getExtendedString(), + "", activityPackage.getSuffix()); + + Map parameters = activityPackage.getParameters(); + + // check that deep link parameters contains the base64 with the 2 keys + assertEquals(activityPackage.getExtendedString(), + "{\"foo\":\"bar\",\"key\":\"value\"}", parameters.get("deeplink_parameters")); + + // check that added and set both session and reattribution package + assertTrue(mockLogger.toString(), mockLogger.containsTestMessage("PackageHandler addPackage")); + assertTrue(mockLogger.toString(), mockLogger.containsTestMessage("PackageHandler sendFirstPackage")); + assertTrue(mockLogger.toString(), mockLogger.containsTestMessage("PackageHandler addPackage")); + assertTrue(mockLogger.toString(), mockLogger.containsTestMessage("PackageHandler sendFirstPackage")); + + // check that sent the reattribution package + assertTrue(mockLogger.toString(), + mockLogger.containsMessage(LogLevel.DEBUG, "Reattribution {key=value, foo=bar}")); + } } diff --git a/README.md b/README.md index 2acb49219..355bd5bb8 100644 --- a/README.md +++ b/README.md @@ -320,16 +320,38 @@ event buffering by adding the following line to your Adjust settings in your ### 13. Disable tracking -You can disable the adjust SDK from tracking by invoking the method `setEnabled` -with the enabled parameter as `false`. This setting is remembered between sessions, but it can only -be activated after the first session. +You can disable the adjust SDK from tracking by invoking the method +`setEnabled` with the enabled parameter as `false`. This setting is remembered +between sessions, but it can only be activated after the first session. ```java Adjust.setEnabled(false); ``` -You can verify if the adjust SDK is currently active with the method `isEnabled`. It is always possible -to activate the adjust SDK by invoking `setEnable` with the enabled parameter as `true`. +You can verify if the adjust SDK is currently active with the method +`isEnabled`. It is always possible to activate the adjust SDK by invoking +`setEnable` with the enabled parameter as `true`. + +### 14. Handle deep linking + +You can also set up the adjust SDK to read deep links that come to your app. We +will only read the data that is injected by adjust tracker URLs. This is +essential if you are planning to run retargeting or re-engagement campaigns +with deep links. + +For each activity that accepts deep links, find the `onCreate` method and add +the folowing call to adjust: + +```java +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Uri data = intent.getData(); + Adjust.appWillOpenUrl(data); + //... +} +``` [adjust.io]: http://adjust.io [dashboard]: http://adjust.io diff --git a/VERSION b/VERSION index 944880fa1..15a279981 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.3.0 diff --git a/doc/migrate.md b/doc/migrate.md index a81730331..872a88e81 100644 --- a/doc/migrate.md +++ b/doc/migrate.md @@ -1,4 +1,4 @@ -## Migrate your adjust SDK for Android to 3.2.0 from v2.1.x +## Migrate your adjust SDK for Android to 3.3.0 from v2.1.x We renamed the main class `com.adeven.adjustio.AdjustIo` to `com.adjust.sdk.Adjust`. Follow these steps to update all adjust SDK calls. @@ -24,7 +24,7 @@ We renamed the main class `com.adeven.adjustio.AdjustIo` to 4. In the same fashion, replace `adeven.adjustio` with `adjust.sdk` in all manifest files to update the package name of the `ReferrerReceiver`. -5. Download version v3.2.0 and create a new Android project from the `Adjust` folder. +5. Download version v3.3.0 and create a new Android project from the `Adjust` folder. ![][import] @@ -36,7 +36,7 @@ We renamed the main class `com.adeven.adjustio.AdjustIo` to 8. Build your project to confirm that everything is properly connected again. -The adjust SDK v3.2.0 added delegate notifications. Check out the [README] for +The adjust SDK v3.3.0 added delegate notifications. Check out the [README] for details.