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.